@atlashub/smartstack-cli 4.34.0 → 4.36.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.
Files changed (50) hide show
  1. package/dist/index.js +28 -32
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +35 -303
  4. package/dist/mcp-entry.mjs.map +1 -1
  5. package/package.json +1 -1
  6. package/templates/skills/apex/references/checks/seed-checks.sh +1 -1
  7. package/templates/skills/apex/references/core-seed-data.md +39 -21
  8. package/templates/skills/application/references/application-roles-template.md +14 -8
  9. package/templates/skills/application/references/provider-template.md +32 -20
  10. package/templates/skills/application/templates-frontend.md +350 -89
  11. package/templates/skills/application/templates-seed.md +23 -11
  12. package/templates/skills/audit-route/SKILL.md +107 -0
  13. package/templates/skills/audit-route/references/routing-pattern.md +129 -0
  14. package/templates/skills/audit-route/steps/step-00-init.md +128 -0
  15. package/templates/skills/audit-route/steps/step-01-inventory.md +157 -0
  16. package/templates/skills/audit-route/steps/step-02-conformity.md +193 -0
  17. package/templates/skills/audit-route/steps/step-03-report.md +201 -0
  18. package/templates/skills/cli-app-sync/SKILL.md +2 -2
  19. package/templates/skills/cli-app-sync/references/comparison-map.md +1 -1
  20. package/templates/skills/dev-start/SKILL.md +12 -2
  21. package/templates/skills/documentation/steps/step-03-validate.md +12 -14
  22. package/templates/skills/efcore/SKILL.md +219 -67
  23. package/templates/agents/efcore/conflicts.md +0 -114
  24. package/templates/agents/efcore/db-deploy.md +0 -86
  25. package/templates/agents/efcore/db-reset.md +0 -98
  26. package/templates/agents/efcore/db-seed.md +0 -73
  27. package/templates/agents/efcore/db-status.md +0 -97
  28. package/templates/agents/efcore/scan.md +0 -124
  29. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +0 -126
  30. package/templates/skills/efcore/references/both-contexts.md +0 -32
  31. package/templates/skills/efcore/references/destructive-operations.md +0 -38
  32. package/templates/skills/efcore/steps/db/step-deploy.md +0 -217
  33. package/templates/skills/efcore/steps/db/step-reset.md +0 -186
  34. package/templates/skills/efcore/steps/db/step-seed.md +0 -166
  35. package/templates/skills/efcore/steps/db/step-status.md +0 -173
  36. package/templates/skills/efcore/steps/migration/step-00-init.md +0 -102
  37. package/templates/skills/efcore/steps/migration/step-01-check.md +0 -164
  38. package/templates/skills/efcore/steps/migration/step-02-create.md +0 -160
  39. package/templates/skills/efcore/steps/migration/step-03-validate.md +0 -168
  40. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +0 -173
  41. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +0 -100
  42. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +0 -115
  43. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +0 -112
  44. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +0 -157
  45. package/templates/skills/efcore/steps/shared/step-00-init.md +0 -131
  46. package/templates/skills/efcore/steps/squash/step-00-init.md +0 -141
  47. package/templates/skills/efcore/steps/squash/step-01-backup.md +0 -120
  48. package/templates/skills/efcore/steps/squash/step-02-fetch.md +0 -168
  49. package/templates/skills/efcore/steps/squash/step-03-create.md +0 -184
  50. package/templates/skills/efcore/steps/squash/step-04-validate.md +0 -174
@@ -2,8 +2,8 @@
2
2
 
3
3
  > These templates generate React/TypeScript code for new applications/modules.
4
4
 
5
- > **DEPRECATED (v3.7+):** Pattern A (mergeRoutes) and Pattern B (JSX Routes) below
6
- > are replaced by PageRegistry.register() + DynamicRouter.
5
+ > **v3.7+:** Pattern A (mergeRoutes) and Pattern B (JSX Routes) were removed.
6
+ > The only supported routing pattern is PageRegistry.register() + DynamicRouter.
7
7
  > See references/frontend-route-wiring-app-tsx.md for the current pattern.
8
8
 
9
9
  ---
@@ -79,6 +79,274 @@ export function $MODULE_PASCALPage() {
79
79
 
80
80
  ---
81
81
 
82
+ ## TEMPLATE: DETAIL PAGE
83
+
84
+ ```tsx
85
+ // pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage.tsx
86
+
87
+ import { useState, useEffect, useCallback } from 'react';
88
+ import { useParams, useNavigate } from 'react-router-dom';
89
+ import { useTranslation } from 'react-i18next';
90
+ import { ArrowLeft, Pencil, Trash2, Loader2 } from 'lucide-react';
91
+ import { Breadcrumb } from '@/components/ui/Breadcrumb';
92
+ import { $moduleApi, type $ENTITY_PASCALDto } from '@/services/api/$moduleApi';
93
+
94
+ export function $MODULE_PASCALDetailPage() {
95
+ // ⚠️ CRITICAL: DynamicRouter generates /:id — use destructuring rename
96
+ const { id: $entityId } = useParams<{ id: string }>();
97
+ const navigate = useNavigate();
98
+ const { t } = useTranslation(['$module', 'common']);
99
+
100
+ const [$entity, set$ENTITY_PASCAL] = useState<$ENTITY_PASCALDto | null>(null);
101
+ const [loading, setLoading] = useState(true);
102
+ const [error, setError] = useState<string | null>(null);
103
+
104
+ const loadData = useCallback(async () => {
105
+ if (!$entityId) return;
106
+ try {
107
+ setLoading(true);
108
+ setError(null);
109
+ const data = await $moduleApi.getById($entityId);
110
+ set$ENTITY_PASCAL(data);
111
+ } catch (err) {
112
+ setError(t('common:errors.loadFailed'));
113
+ console.error('Failed to load $entity:', err);
114
+ } finally {
115
+ setLoading(false);
116
+ }
117
+ }, [$entityId, t]);
118
+
119
+ useEffect(() => {
120
+ loadData();
121
+ }, [loadData]);
122
+
123
+ const handleDelete = async () => {
124
+ if (!$entityId || !confirm(t('common:confirmDelete'))) return;
125
+ try {
126
+ await $moduleApi.delete($entityId);
127
+ navigate('..');
128
+ } catch (err) {
129
+ console.error('Failed to delete:', err);
130
+ }
131
+ };
132
+
133
+ if (loading) {
134
+ return (
135
+ <div className="flex items-center justify-center py-12">
136
+ <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
137
+ </div>
138
+ );
139
+ }
140
+
141
+ if (error || !$entity) {
142
+ return (
143
+ <div className="p-6">
144
+ <p className="text-[var(--error-text)]">{error || t('common:errors.notFound')}</p>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ return (
150
+ <div className="space-y-6">
151
+ <Breadcrumb
152
+ items={[
153
+ { label: t('$module:title'), href: '/$APPLICATION_KEBAB/$MODULE_KEBAB' },
154
+ { label: $entity.name },
155
+ ]}
156
+ />
157
+
158
+ <div className="flex items-center justify-between">
159
+ <div className="flex items-center gap-3">
160
+ <button onClick={() => navigate('..')} className="p-2 hover:bg-[var(--bg-hover)]" style={{ borderRadius: 'var(--radius-button)' }}>
161
+ <ArrowLeft className="w-5 h-5" />
162
+ </button>
163
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">{$entity.name}</h1>
164
+ </div>
165
+ <div className="flex items-center gap-2">
166
+ <button
167
+ onClick={() => navigate('edit')}
168
+ className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors"
169
+ style={{ borderRadius: 'var(--radius-button)' }}
170
+ >
171
+ <Pencil className="w-4 h-4" />
172
+ {t('common:actions.edit')}
173
+ </button>
174
+ <button
175
+ onClick={handleDelete}
176
+ className="flex items-center gap-2 px-4 py-2 bg-[var(--error-bg)] hover:bg-[var(--error-border)] text-[var(--error-text)] font-medium transition-colors"
177
+ style={{ borderRadius: 'var(--radius-button)' }}
178
+ >
179
+ <Trash2 className="w-4 h-4" />
180
+ {t('common:actions.delete')}
181
+ </button>
182
+ </div>
183
+ </div>
184
+
185
+ {/* Detail content — adapt to your entity */}
186
+ <div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] p-6" style={{ borderRadius: 'var(--radius-card)' }}>
187
+ <dl className="grid grid-cols-1 md:grid-cols-2 gap-4">
188
+ <div>
189
+ <dt className="text-sm font-medium text-[var(--text-secondary)]">{t('$module:columns.name')}</dt>
190
+ <dd className="mt-1 text-[var(--text-primary)]">{$entity.name}</dd>
191
+ </div>
192
+ <div>
193
+ <dt className="text-sm font-medium text-[var(--text-secondary)]">{t('$module:columns.description')}</dt>
194
+ <dd className="mt-1 text-[var(--text-primary)]">{$entity.description || '-'}</dd>
195
+ </div>
196
+ </dl>
197
+ </div>
198
+ </div>
199
+ );
200
+ }
201
+ ```
202
+
203
+ ---
204
+
205
+ ## TEMPLATE: EDIT PAGE
206
+
207
+ ```tsx
208
+ // pages/$APPLICATION/$MODULE/$MODULE_PASCALEditPage.tsx
209
+
210
+ import { useState, useEffect, useCallback } from 'react';
211
+ import { useParams, useNavigate } from 'react-router-dom';
212
+ import { useTranslation } from 'react-i18next';
213
+ import { ArrowLeft, Save, Loader2 } from 'lucide-react';
214
+ import { Breadcrumb } from '@/components/ui/Breadcrumb';
215
+ import { $moduleApi, type $ENTITY_PASCALDto, type Update$ENTITY_PASCALRequest } from '@/services/api/$moduleApi';
216
+
217
+ export function $MODULE_PASCALEditPage() {
218
+ // ⚠️ CRITICAL: DynamicRouter generates /:id/edit — use destructuring rename
219
+ const { id: $entityId } = useParams<{ id: string }>();
220
+ const navigate = useNavigate();
221
+ const { t } = useTranslation(['$module', 'common']);
222
+
223
+ const [data, setData] = useState<$ENTITY_PASCALDto | null>(null);
224
+ const [form, setForm] = useState<Update$ENTITY_PASCALRequest>({ name: '', description: '' });
225
+ const [loading, setLoading] = useState(true);
226
+ const [saving, setSaving] = useState(false);
227
+ const [error, setError] = useState<string | null>(null);
228
+
229
+ const loadData = useCallback(async () => {
230
+ if (!$entityId) return;
231
+ try {
232
+ setLoading(true);
233
+ const result = await $moduleApi.getById($entityId);
234
+ setData(result);
235
+ setForm({ name: result.name, description: result.description || '' });
236
+ } catch (err) {
237
+ setError(t('common:errors.loadFailed'));
238
+ console.error('Failed to load $entity:', err);
239
+ } finally {
240
+ setLoading(false);
241
+ }
242
+ }, [$entityId, t]);
243
+
244
+ useEffect(() => {
245
+ loadData();
246
+ }, [loadData]);
247
+
248
+ const handleSave = async () => {
249
+ if (!$entityId) return;
250
+ try {
251
+ setSaving(true);
252
+ setError(null);
253
+ await $moduleApi.update($entityId, form);
254
+ navigate('..');
255
+ } catch (err) {
256
+ setError(t('common:errors.saveFailed'));
257
+ console.error('Failed to save:', err);
258
+ } finally {
259
+ setSaving(false);
260
+ }
261
+ };
262
+
263
+ if (loading) {
264
+ return (
265
+ <div className="flex items-center justify-center py-12">
266
+ <Loader2 className="w-8 h-8 animate-spin text-[var(--color-accent-500)]" />
267
+ </div>
268
+ );
269
+ }
270
+
271
+ return (
272
+ <div className="space-y-6">
273
+ <Breadcrumb
274
+ items={[
275
+ { label: t('$module:title'), href: '/$APPLICATION_KEBAB/$MODULE_KEBAB' },
276
+ { label: data?.name || '...', href: '..' },
277
+ { label: t('common:actions.edit') },
278
+ ]}
279
+ />
280
+
281
+ <div className="flex items-center gap-3">
282
+ <button onClick={() => navigate('..')} className="p-2 hover:bg-[var(--bg-hover)]" style={{ borderRadius: 'var(--radius-button)' }}>
283
+ <ArrowLeft className="w-5 h-5" />
284
+ </button>
285
+ <h1 className="text-2xl font-bold text-[var(--text-primary)]">
286
+ {t('common:actions.edit')} — {data?.name}
287
+ </h1>
288
+ </div>
289
+
290
+ {error && (
291
+ <div className="p-4 bg-[var(--error-bg)] border border-[var(--error-border)]" style={{ borderRadius: 'var(--radius-card)' }}>
292
+ <span className="text-[var(--error-text)]">{error}</span>
293
+ </div>
294
+ )}
295
+
296
+ <div className="bg-[var(--bg-card)] border border-[var(--item-color-border)] p-6" style={{ borderRadius: 'var(--radius-card)' }}>
297
+ <div className="space-y-4 max-w-lg">
298
+ <div>
299
+ <label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
300
+ {t('$module:columns.name')}
301
+ </label>
302
+ <input
303
+ type="text"
304
+ value={form.name}
305
+ onChange={(e) => setForm(prev => ({ ...prev, name: e.target.value }))}
306
+ className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
307
+ style={{ borderRadius: 'var(--radius-input)' }}
308
+ />
309
+ </div>
310
+ <div>
311
+ <label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
312
+ {t('$module:columns.description')}
313
+ </label>
314
+ <textarea
315
+ value={form.description || ''}
316
+ onChange={(e) => setForm(prev => ({ ...prev, description: e.target.value }))}
317
+ rows={3}
318
+ className="w-full px-3 py-2 bg-[var(--bg-primary)] border border-[var(--border-color)] text-sm focus:outline-none focus:border-[var(--color-accent-500)]"
319
+ style={{ borderRadius: 'var(--radius-input)' }}
320
+ />
321
+ </div>
322
+ </div>
323
+
324
+ <div className="flex justify-end gap-3 mt-6 pt-4 border-t border-[var(--border-color)]">
325
+ <button
326
+ onClick={() => navigate('..')}
327
+ className="px-4 py-2 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] text-[var(--text-primary)] font-medium transition-colors"
328
+ style={{ borderRadius: 'var(--radius-button)' }}
329
+ >
330
+ {t('common:actions.cancel')}
331
+ </button>
332
+ <button
333
+ onClick={handleSave}
334
+ disabled={saving}
335
+ className="flex items-center gap-2 px-4 py-2 bg-[var(--color-accent-500)] hover:bg-[var(--color-accent-600)] text-white font-medium transition-colors disabled:opacity-50"
336
+ style={{ borderRadius: 'var(--radius-button)' }}
337
+ >
338
+ {saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
339
+ {t('common:actions.save')}
340
+ </button>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ );
345
+ }
346
+ ```
347
+
348
+ ---
349
+
82
350
  ## TEMPLATE: LIST VIEW (Reusable component)
83
351
 
84
352
  ```tsx
@@ -507,117 +775,108 @@ export const $moduleApi = {
507
775
 
508
776
  ---
509
777
 
510
- ## TEMPLATE: ROUTES (App.tsx)
511
-
512
- ### Detect App.tsx Routing Pattern FIRST
778
+ ## TEMPLATE: ROUTES (PageRegistry + DynamicRouter)
513
779
 
514
- Before adding routes, **read App.tsx** and detect which pattern is used:
780
+ ### Default Pattern (v3.7+): PageRegistry + DynamicRouter
515
781
 
516
- | Pattern | How to detect | Action |
517
- |---------|---------------|--------|
518
- | **Pattern A** (mergeRoutes) | `applicationRoutes: ApplicationRouteExtensions` present | Add to `applicationRoutes.{application}[]` array |
519
- | **Pattern B** (JSX Routes) | `<Route path="/{application}" element={<{Layout} />}>` present | Insert `<Route>` children inside Layout wrapper |
782
+ > **This is the ONLY supported pattern** since v3.7.
783
+ > Pages are registered in `componentRegistry.generated.ts` via `PageRegistry.register()`.
784
+ > DynamicRouter resolves routes automatically from the navigation API + PageRegistry.
785
+ > **No App.tsx wiring needed.** No manual route duplication for tenant prefixes.
520
786
 
521
- ### Pattern A: mergeRoutes (applicationRoutes array)
787
+ **Steps:**
522
788
 
523
- > **This is the DEFAULT pattern** generated by `smartstack init`.
524
- > Routes added to `applicationRoutes` are automatically injected into BOTH standard and tenant-prefixed route trees by `mergeRoutes()`. No manual duplication needed.
789
+ 1. Create your page component(s)
790
+ 2. Register in `componentRegistry.generated.ts` (or run `scaffold_routes outputFormat="componentRegistry"`)
791
+ 3. Ensure navigation seed data has matching `ComponentKey` values
792
+ 4. DynamicRouter does the rest
525
793
 
526
794
  ```tsx
527
- // Add to App.tsx imports at top
528
- import { $MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage';
529
- import { $MODULE_PASCALDetailPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage';
530
- import { Create$MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage';
531
-
532
- // Add routes to applicationRoutes.{application}[] with RELATIVE paths (no leading /)
533
- const applicationRoutes: ApplicationRouteExtensions = {
534
- '$APPLICATION_KEBAB': [
535
- // ... existing routes ...
536
- { path: '$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
537
- { path: '$MODULE_KEBAB/create', element: <Create$MODULE_PASCALPage /> },
538
- { path: '$MODULE_KEBAB/:id', element: <$MODULE_PASCALDetailPage /> },
539
- { path: '$MODULE_KEBAB/:id/edit', element: <Create$MODULE_PASCALPage /> },
540
- ],
541
- };
795
+ // 1. Create page component
796
+ // pages/$APPLICATION/$MODULE/$MODULE_PASCALPage.tsx
797
+ export function $MODULE_PASCALPage() { /* ... */ }
798
+
799
+ // 2. Register in componentRegistry.generated.ts
800
+ import { PageRegistry } from '@/extensions/PageRegistry';
801
+
802
+ PageRegistry.register(
803
+ '$APPLICATION_KEBAB.$MODULE_KEBAB',
804
+ lazy(() => import('@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage')
805
+ .then(m => ({ default: m.$MODULE_PASCALPage })))
806
+ );
807
+
808
+ // For CRUD sub-pages (detail, create):
809
+ PageRegistry.register(
810
+ '$APPLICATION_KEBAB.$MODULE_KEBAB.detail',
811
+ lazy(() => import('@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage')
812
+ .then(m => ({ default: m.$MODULE_PASCALDetailPage })))
813
+ );
814
+
815
+ PageRegistry.register(
816
+ '$APPLICATION_KEBAB.$MODULE_KEBAB.create',
817
+ lazy(() => import('@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage')
818
+ .then(m => ({ default: m.Create$MODULE_PASCALPage })))
819
+ );
542
820
  ```
543
821
 
544
- **mergeRoutes auto-generates redirects** for intermediate paths (e.g., `$APPLICATION` → `$APPLICATION/$DEFAULT_MODULE_KEBAB`) so you don't need to add index redirects manually.
822
+ ```sql
823
+ -- 3. Navigation seed data (ComponentKey must match PageRegistry key)
824
+ INSERT INTO core.nav_Sections (...)
825
+ VALUES (..., ComponentKey = '$APPLICATION_KEBAB.$MODULE_KEBAB', ...);
826
+ ```
545
827
 
546
- **Application-to-Layout mapping (automatic via mergeRoutes):**
828
+ ### ⚠️ IMPLICIT ROUTE PARAM CONVENTION
547
829
 
548
- | Application key | Injected into Layout | Standard path | Tenant path |
549
- |-----------------|---------------------|---------------|-------------|
550
- | `administration` | `AppLayout` | `/administration/...` | `/t/:slug/administration/...` |
551
- | `{application}` | `AppLayout` | `/{application}/...` | `/t/:slug/{application}/...` |
552
- | `myspace` | `AppLayout` | `/myspace/...` | `/t/:slug/myspace/...` |
830
+ DynamicRouter generates implicit sub-routes for `.detail`, `.create`, and `.edit` page keys:
553
831
 
554
- ### Pattern B: JSX Routes (inside Layout wrapper)
832
+ | Suffix | URL pattern | Route param |
833
+ |--------|-------------|-------------|
834
+ | `.detail` | `/:id` | `id` |
835
+ | `.create` | `/create` | — |
836
+ | `.edit` | `/:id/edit` | `id` |
555
837
 
556
- > **Legacy pattern** only used if App.tsx was manually restructured with JSX `<Route>` elements.
838
+ **The route param is ALWAYS `id`** never `ticketId`, `userId`, or any entity-specific name.
557
839
 
558
- The unified `AppLayout` provides the application shell: **header with AvatarMenu**, sidebar, navigation. It renders child pages via React Router's `<Outlet />`.
840
+ In detail/edit pages, use destructuring rename to keep a readable local variable:
559
841
 
560
- **If routes are placed OUTSIDE the layout wrapper, the shell (header, sidebar, AvatarMenu) will NOT render. The page appears "naked" without any navigation.**
842
+ ```tsx
843
+ // ✅ CORRECT — matches DynamicRouter's /:id pattern
844
+ const { id: $entityId } = useParams<{ id: string }>();
561
845
 
562
- **Step-by-step insertion:**
846
+ // ❌ WRONG — will be undefined because the route param is :id, not :$entityId
847
+ const { $entityId } = useParams<{ $entityId: string }>();
848
+ ```
563
849
 
564
- 1. Open `App.tsx`
565
- 2. Find the existing layout route for the target application:
566
- - `administration` → `<Route path="/administration" element={<AppLayout />}>`
567
- - `{application}` → `<Route path="/{application}" element={<AppLayout />}>`
568
- - `myspace` → `<Route path="/myspace" element={<AppLayout />}>`
569
- 3. Add the new routes **INSIDE** that `<Route>` block
570
- 4. If a tenant-prefixed block exists (`/t/:slug/...`), add the routes there too
850
+ ### How DynamicRouter resolves routes
571
851
 
572
- ```tsx
573
- // Add to App.tsx
574
-
575
- import { $MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALPage';
576
- import { $MODULE_PASCALDetailPage } from '@/pages/$APPLICATION/$MODULE/$MODULE_PASCALDetailPage';
577
- import { Create$MODULE_PASCALPage } from '@/pages/$APPLICATION/$MODULE/Create$MODULE_PASCALPage';
578
-
579
- // Find the EXISTING layout route and add routes INSIDE it:
580
- <Route path="/$APPLICATION" element={<$APPLICATION_Layout />}>
581
- {/* ... existing routes stay here ... */}
582
-
583
- {/* NEW: $MODULE routes - added as children of the layout */}
584
- <Route path="$MODULE_KEBAB">
585
- <Route index element={<Navigate to="." replace />} />
586
- <Route index element={<$MODULE_PASCALPage />} />
587
- <Route path="create" element={<Create$MODULE_PASCALPage />} />
588
- <Route path=":id" element={<$MODULE_PASCALDetailPage />} />
589
- <Route path=":id/edit" element={<Create$MODULE_PASCALPage />} />
590
- </Route>
591
- </Route>
592
- ```
852
+ 1. Fetches menu from `GET /api/navigation/menu`
853
+ 2. For each Application → Module → Section → Resource, builds `<Route>` elements
854
+ 3. Looks up each item's `ComponentKey` in `PageRegistry` to get the React component
855
+ 4. Wraps routes in `<ProtectedRoute>` (API-driven permission check via `isOpen` flag)
856
+ 5. Generates both standard (`/$APPLICATION/$MODULE`) and tenant-prefixed (`/t/:slug/$APPLICATION/$MODULE`) routes automatically
593
857
 
594
- ### ⚠️ CRITICAL RULES (both patterns)
858
+ ### ⚠️ CRITICAL RULES
595
859
 
596
- **FORBIDDEN — Adding to clientRoutes[] with absolute paths:**
860
+ **FORBIDDEN — Flat routes outside DynamicRouter:**
597
861
  ```tsx
598
- // ❌ WRONG — bypasses layout entirely, shell will NOT render
599
- const clientRoutes: RouteConfig[] = [
600
- { path: '/$APPLICATION_KEBAB/$MODULE_KEBAB', element: <$MODULE_PASCALPage /> },
601
- ];
862
+ // ❌ WRONG — bypasses layout, permission checks, and tenant handling
863
+ <Route path="/$APPLICATION_KEBAB/$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
602
864
  ```
603
865
 
604
- `clientRoutes` is ONLY for routes **outside** SmartStack locked applications (e.g., `/about`, `/pricing`).
605
-
606
- **FORBIDDEN — Flat routes outside layout:**
866
+ **FORBIDDEN Registering pages without navigation seed data:**
607
867
  ```tsx
608
- // ❌ WRONG (Pattern B only) flat route bypasses layout
609
- <Route path="/$APPLICATION_KEBAB/$MODULE_KEBAB" element={<$MODULE_PASCALPage />} />
868
+ // ❌ WRONG — PageRegistry.register() alone does nothing without a matching ComponentKey in the DB
869
+ PageRegistry.register('my-app.my-module', MyPage);
870
+ // Must also have INSERT INTO core.nav_Sections with ComponentKey = 'my-app.my-module'
610
871
  ```
611
872
 
612
- ### Why nested/context routes?
873
+ ---
874
+
875
+ ### Legacy Patterns (DEPRECATED — removed in v3.7)
613
876
 
614
- | Aspect | clientRoutes (wrong) | applicationRoutes / nested (correct) |
615
- |--------|---------------------|--------------------------------------|
616
- | Shell rendered | No (bypasses layout) | Yes (Outlet pattern) |
617
- | AvatarMenu visible | No | Yes |
618
- | Tenant prefix | Manual duplication | Automatic (mergeRoutes) |
619
- | Auto-redirects | None | Generated for intermediate paths |
620
- | Permission check | Bypassed (no RouteGuard) | Enforced by RouteGuard |
877
+ > **Pattern A (mergeRoutes)** and **Pattern B (JSX Routes)** were removed in v3.7.
878
+ > `smartstackRoutes.tsx`, `RouteGuard.tsx`, and `mergeRoutes()` no longer exist.
879
+ > If you encounter a pre-v3.7 project, upgrade to PageRegistry + DynamicRouter.
621
880
 
622
881
  ---
623
882
 
@@ -649,11 +908,13 @@ These patterns are **strictly prohibited** in generated frontend code:
649
908
  | Check | Status |
650
909
  |-------|--------|
651
910
  | ☐ Main page created (`$MODULE_PASCALPage.tsx`) | |
911
+ | ☐ Detail page created (`$MODULE_PASCALDetailPage.tsx`) with `useParams<{ id: string }>` | |
912
+ | ☐ Edit page created (`$MODULE_PASCALEditPage.tsx`) with `useParams<{ id: string }>` | |
652
913
  | ☐ ListView component created (`$MODULE_PASCALListView.tsx`) | |
653
914
  | ☐ Preferences hook created (`use$MODULE_PASCALPreferences.ts`) | |
654
915
  | ☐ API service created (uses `apiClient`, NOT raw axios) | |
655
- | ☐ Routes added inside Layout wrapper in App.tsx | |
656
- | ☐ Route path follows `/{application_kebab}/{module_kebab}` (kebab-case) | |
916
+ | ☐ PageRegistry keys registered (`.detail`, `.create`, `.edit`) in `componentRegistry.generated.ts` | |
917
+ | ☐ Route param uses `{ id: entityId }` destructuring (NOT `{ entityId }`) | |
657
918
 
658
919
  ### Theme Compliance
659
920
  | Check | Status |
@@ -805,7 +805,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
805
805
  else
806
806
  {
807
807
  app = NavigationApplication.Create(
808
- "{app_code}", "{app_label_en}",
808
+ ApplicationZone.Business, "{app_code}", "{app_label_en}",
809
809
  "{app_desc_en}", "{app_icon}", IconType.Lucide,
810
810
  "/{app_code}", {display_order});
811
811
  context.NavigationApplications.Add(app);
@@ -822,17 +822,23 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
822
822
 
823
823
  // Module translations — IDEMPOTENT (unique index IX_nav_Translations_EntityType_EntityId_LanguageCode)
824
824
  // CRITICAL: Always check before inserting to avoid duplicate key errors on re-runs
825
+ // CRITICAL: Use modEntity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
825
826
  // if (!await context.NavigationTranslations.AnyAsync(
826
- // t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId && t.EntityType == NavigationEntityType.Module, ct))
827
- // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries()) { ... } }
827
+ // t => t.EntityId == mod1Entity.Id && t.EntityType == NavigationEntityType.Module, ct))
828
+ // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
829
+ // { context.NavigationTranslations.Add(
830
+ // NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description)); } }
828
831
 
829
832
  // Sections (idempotent per-section — check each before inserting)
830
833
  // var secExists = await context.NavigationSections.AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == ..., ct);
831
834
  // Use NavigationSection.Create(moduleId, code, label, description, icon, iconType, route, displayOrder)
832
835
 
833
836
  // Section translations — IDEMPOTENT (same guard pattern as module translations)
837
+ // CRITICAL: Resolve actual section from DB (secEntry.Id is seed-time GUID ≠ DB ID):
838
+ // var actualSection = await context.NavigationSections
839
+ // .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
834
840
  // if (!await context.NavigationTranslations.AnyAsync(
835
- // t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
841
+ // t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
836
842
 
837
843
  // Resources (idempotent per-resource — check each before inserting)
838
844
  // var resExists = await context.NavigationResources.AnyAsync(r => r.Code == resEntry.Code && r.SectionId == ..., ct);
@@ -841,20 +847,26 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
841
847
 
842
848
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
843
849
  {
844
- // Check idempotence
850
+ // Resolve application from DB by Code — NOT seed-time GUID
851
+ var app = await context.NavigationApplications
852
+ .FirstAsync(a => a.Code == "{app_code}", ct);
853
+
854
+ // Check idempotence — scoped by actual DB ApplicationId
845
855
  var exists = await context.Roles
846
- .AnyAsync(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId, ct);
856
+ .AnyAsync(r => r.ApplicationId == app.Id, ct);
847
857
  if (exists) return;
848
858
 
849
859
  // Create application-scoped roles (Admin, Manager, Contributor, Viewer)
850
860
  foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
851
861
  {
852
862
  var role = Role.Create(
853
- entry.Code,
854
- entry.Name,
855
- entry.Description,
856
- entry.ApplicationId,
857
- entry.IsSystem);
863
+ name: entry.Name,
864
+ shortName: entry.Code,
865
+ category: RoleCategory.Application,
866
+ description: entry.Description,
867
+ isSystem: entry.IsSystem,
868
+ applicationId: app.Id,
869
+ code: entry.Code);
858
870
  context.Roles.Add(role);
859
871
  }
860
872
  await ((DbContext)context).SaveChangesAsync(ct);