@atlashub/smartstack-cli 4.35.0 → 4.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +54 -100
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +54 -11
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/efcore/migration.md +43 -0
- package/templates/agents/efcore/rebase-snapshot.md +36 -0
- package/templates/agents/efcore/squash.md +36 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +1 -1
- package/templates/skills/apex/references/core-seed-data.md +39 -21
- package/templates/skills/application/references/application-roles-template.md +14 -8
- package/templates/skills/application/references/provider-template.md +32 -20
- package/templates/skills/application/templates-frontend.md +294 -2
- package/templates/skills/application/templates-seed.md +23 -11
- package/templates/skills/audit-route/SKILL.md +107 -0
- package/templates/skills/audit-route/references/routing-pattern.md +129 -0
- package/templates/skills/audit-route/steps/step-00-init.md +128 -0
- package/templates/skills/audit-route/steps/step-01-inventory.md +157 -0
- package/templates/skills/audit-route/steps/step-02-conformity.md +193 -0
- package/templates/skills/audit-route/steps/step-03-report.md +201 -0
- package/templates/skills/dev-start/SKILL.md +12 -2
- package/templates/skills/efcore/SKILL.md +219 -67
- package/templates/agents/efcore/conflicts.md +0 -114
- package/templates/agents/efcore/db-deploy.md +0 -86
- package/templates/agents/efcore/db-reset.md +0 -98
- package/templates/agents/efcore/db-seed.md +0 -73
- package/templates/agents/efcore/db-status.md +0 -97
- package/templates/agents/efcore/scan.md +0 -124
- package/templates/skills/efcore/references/both-contexts.md +0 -32
- package/templates/skills/efcore/references/destructive-operations.md +0 -38
- package/templates/skills/efcore/steps/db/step-deploy.md +0 -217
- package/templates/skills/efcore/steps/db/step-reset.md +0 -186
- package/templates/skills/efcore/steps/db/step-seed.md +0 -166
- package/templates/skills/efcore/steps/db/step-status.md +0 -173
- package/templates/skills/efcore/steps/migration/step-00-init.md +0 -102
- package/templates/skills/efcore/steps/migration/step-01-check.md +0 -164
- package/templates/skills/efcore/steps/migration/step-02-create.md +0 -160
- package/templates/skills/efcore/steps/migration/step-03-validate.md +0 -168
- package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +0 -173
- package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +0 -100
- package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +0 -115
- package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +0 -112
- package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +0 -157
- package/templates/skills/efcore/steps/shared/step-00-init.md +0 -131
- package/templates/skills/efcore/steps/squash/step-00-init.md +0 -141
- package/templates/skills/efcore/steps/squash/step-01-backup.md +0 -120
- package/templates/skills/efcore/steps/squash/step-02-fetch.md +0 -168
- package/templates/skills/efcore/steps/squash/step-03-create.md +0 -184
- package/templates/skills/efcore/steps/squash/step-04-validate.md +0 -174
|
@@ -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
|
|
@@ -557,6 +825,28 @@ INSERT INTO core.nav_Sections (...)
|
|
|
557
825
|
VALUES (..., ComponentKey = '$APPLICATION_KEBAB.$MODULE_KEBAB', ...);
|
|
558
826
|
```
|
|
559
827
|
|
|
828
|
+
### ⚠️ IMPLICIT ROUTE PARAM CONVENTION
|
|
829
|
+
|
|
830
|
+
DynamicRouter generates implicit sub-routes for `.detail`, `.create`, and `.edit` page keys:
|
|
831
|
+
|
|
832
|
+
| Suffix | URL pattern | Route param |
|
|
833
|
+
|--------|-------------|-------------|
|
|
834
|
+
| `.detail` | `/:id` | `id` |
|
|
835
|
+
| `.create` | `/create` | — |
|
|
836
|
+
| `.edit` | `/:id/edit` | `id` |
|
|
837
|
+
|
|
838
|
+
**The route param is ALWAYS `id`** — never `ticketId`, `userId`, or any entity-specific name.
|
|
839
|
+
|
|
840
|
+
In detail/edit pages, use destructuring rename to keep a readable local variable:
|
|
841
|
+
|
|
842
|
+
```tsx
|
|
843
|
+
// ✅ CORRECT — matches DynamicRouter's /:id pattern
|
|
844
|
+
const { id: $entityId } = useParams<{ id: string }>();
|
|
845
|
+
|
|
846
|
+
// ❌ WRONG — will be undefined because the route param is :id, not :$entityId
|
|
847
|
+
const { $entityId } = useParams<{ $entityId: string }>();
|
|
848
|
+
```
|
|
849
|
+
|
|
560
850
|
### How DynamicRouter resolves routes
|
|
561
851
|
|
|
562
852
|
1. Fetches menu from `GET /api/navigation/menu`
|
|
@@ -618,11 +908,13 @@ These patterns are **strictly prohibited** in generated frontend code:
|
|
|
618
908
|
| Check | Status |
|
|
619
909
|
|-------|--------|
|
|
620
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 }>` | |
|
|
621
913
|
| ☐ ListView component created (`$MODULE_PASCALListView.tsx`) | |
|
|
622
914
|
| ☐ Preferences hook created (`use$MODULE_PASCALPreferences.ts`) | |
|
|
623
915
|
| ☐ API service created (uses `apiClient`, NOT raw axios) | |
|
|
624
|
-
| ☐
|
|
625
|
-
| ☐ Route
|
|
916
|
+
| ☐ PageRegistry keys registered (`.detail`, `.create`, `.edit`) in `componentRegistry.generated.ts` | |
|
|
917
|
+
| ☐ Route param uses `{ id: entityId }` destructuring (NOT `{ entityId }`) | |
|
|
626
918
|
|
|
627
919
|
### Theme Compliance
|
|
628
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 ==
|
|
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 ==
|
|
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
|
-
//
|
|
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 ==
|
|
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.
|
|
854
|
-
entry.
|
|
855
|
-
|
|
856
|
-
entry.
|
|
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);
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-route
|
|
3
|
+
description: |
|
|
4
|
+
Audit dynamic routing architecture against the DB-driven pattern.
|
|
5
|
+
Use this skill when:
|
|
6
|
+
- Verifying routing is fully database-driven (no hardcoded routes)
|
|
7
|
+
- Checking the 5 mandatory elements (componentRegistry, ProtectedRoute, DynamicRouter, useRouteConfig, API)
|
|
8
|
+
- Validating route/permission alignment
|
|
9
|
+
- Detecting anti-patterns (static routes, hardcoded permissions, non-lazy imports)
|
|
10
|
+
argument-hint: "[--strict] [--scope=frontend|backend|all]"
|
|
11
|
+
model: sonnet
|
|
12
|
+
allowed-tools: "Read, Grep, Glob, Bash, Agent, ToolSearch"
|
|
13
|
+
entry_point: steps/step-00-init.md
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<objective>
|
|
17
|
+
Audit the dynamic routing implementation against the SmartStack DB-driven routing pattern. The architecture requires that navigation and permissions are **entirely driven by the database** — the React frontend must contain **zero hardcoded routes**.
|
|
18
|
+
|
|
19
|
+
The audit validates 5 mandatory elements:
|
|
20
|
+
1. **componentRegistry** — static mapping `string -> lazy(component)`, only allowed static file
|
|
21
|
+
2. **ProtectedRoute** — front-end guard that validates permissions from the API (defense in depth)
|
|
22
|
+
3. **DynamicRouter** — rendering engine that generates `<Route>` from API config
|
|
23
|
+
4. **useRouteConfig** — hook that fetches route config + permissions from the API at mount
|
|
24
|
+
5. **Navigation API** — endpoint returning routes **already filtered** by JWT/session
|
|
25
|
+
|
|
26
|
+
For each element: inventory, conformity check, causal analysis of deviations, and migration plan.
|
|
27
|
+
</objective>
|
|
28
|
+
|
|
29
|
+
<quick_start>
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
/audit-route # Full audit (frontend + backend)
|
|
33
|
+
/audit-route --scope=frontend # Frontend only (React routing)
|
|
34
|
+
/audit-route --scope=backend # Backend only (API + navigation)
|
|
35
|
+
/audit-route --strict # Fail on any partial conformity (treat warnings as errors)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
</quick_start>
|
|
39
|
+
|
|
40
|
+
<parameters>
|
|
41
|
+
|
|
42
|
+
<flags>
|
|
43
|
+
| Flag | Description |
|
|
44
|
+
|------|-------------|
|
|
45
|
+
| `--scope=all` | Audit both frontend and backend (default) |
|
|
46
|
+
| `--scope=frontend` | Audit React routing only (elements 1-4) |
|
|
47
|
+
| `--scope=backend` | Audit API navigation only (element 5) |
|
|
48
|
+
| `--strict` | Treat partial conformity (warning) as non-conformity (error) |
|
|
49
|
+
</flags>
|
|
50
|
+
|
|
51
|
+
</parameters>
|
|
52
|
+
|
|
53
|
+
<workflow>
|
|
54
|
+
1. **Initialize** — Detect project structure, resolve paths, parse flags
|
|
55
|
+
2. **MCP Pre-scan** — Call `validate_frontend_routes` and `validate_conventions` for automated baseline
|
|
56
|
+
3. **Inventory** — Locate each of the 5 mandatory elements in the codebase
|
|
57
|
+
4. **Conformity** — Evaluate each element against the reference pattern, cross-reference with MCP findings
|
|
58
|
+
5. **Anti-patterns** — Scan for hardcoded routes, static permissions, non-lazy imports
|
|
59
|
+
6. **Report** — Generate structured audit report with causal analysis and migration plan
|
|
60
|
+
</workflow>
|
|
61
|
+
|
|
62
|
+
<state_variables>
|
|
63
|
+
| Variable | Type | Description |
|
|
64
|
+
|----------|------|-------------|
|
|
65
|
+
| `{scope}` | string | frontend, backend, or all |
|
|
66
|
+
| `{strict}` | boolean | Treat warnings as errors |
|
|
67
|
+
| `{web_root}` | string | Path to web/smartstack-web/src/ |
|
|
68
|
+
| `{api_root}` | string | Path to src/SmartStack.Api/ |
|
|
69
|
+
| `{elements}` | object[] | Inventory of the 5 mandatory elements |
|
|
70
|
+
| `{conformity}` | object[] | Conformity status per element |
|
|
71
|
+
| `{anti_patterns}` | object[] | Detected anti-patterns with file:line |
|
|
72
|
+
| `{mcp_results}` | object | Results from MCP validate_frontend_routes + validate_conventions |
|
|
73
|
+
| `{score}` | string | X/5 conformity score |
|
|
74
|
+
</state_variables>
|
|
75
|
+
|
|
76
|
+
<entry_point>
|
|
77
|
+
|
|
78
|
+
**FIRST ACTION:** Load `steps/step-00-init.md`
|
|
79
|
+
|
|
80
|
+
</entry_point>
|
|
81
|
+
|
|
82
|
+
<step_files>
|
|
83
|
+
| Step | File | Purpose |
|
|
84
|
+
|------|------|---------|
|
|
85
|
+
| 00 | `steps/step-00-init.md` | Parse flags, detect project structure, resolve paths |
|
|
86
|
+
| 01 | `steps/step-01-inventory.md` | Locate all 5 mandatory elements in the codebase |
|
|
87
|
+
| 02 | `steps/step-02-conformity.md` | Evaluate conformity, causal analysis, anti-pattern scan |
|
|
88
|
+
| 03 | `steps/step-03-report.md` | Generate structured audit report with migration plan |
|
|
89
|
+
</step_files>
|
|
90
|
+
|
|
91
|
+
<execution_rules>
|
|
92
|
+
- **Read-only** — This skill NEVER modifies code
|
|
93
|
+
- **Non-judgmental** — Analyze code, not developers. Distinguish errors from conscious choices
|
|
94
|
+
- **Comprehensive** — Check ALL files, not a sample. Grep entire codebase for anti-patterns
|
|
95
|
+
- **Evidence-based** — Every finding must reference a specific file:line
|
|
96
|
+
- **Causal** — Every deviation must include: what was done instead, why it deviates, probable reason, consequence, and migration path
|
|
97
|
+
- **SmartStack-aware** — Respect SmartStack SDK architecture (PageRegistry, DynamicRouter, menu API)
|
|
98
|
+
</execution_rules>
|
|
99
|
+
|
|
100
|
+
<success_criteria>
|
|
101
|
+
- All 5 elements inventoried with exact file paths and line numbers
|
|
102
|
+
- Each element rated: Conforme / Partiellement conforme / Non conforme / Absent
|
|
103
|
+
- Anti-patterns detected across entire codebase (not just routing files)
|
|
104
|
+
- Causal analysis for every deviation (5 questions answered)
|
|
105
|
+
- Prioritized migration plan (critical > important > nice-to-have)
|
|
106
|
+
- Executive summary with X/5 score and 3-line architecture status
|
|
107
|
+
</success_criteria>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Dynamic Routing Reference Pattern
|
|
2
|
+
|
|
3
|
+
## Architecture Principle
|
|
4
|
+
|
|
5
|
+
Navigation and permissions are **entirely driven by the database**. The React frontend contains **zero hardcoded routes**. The backend API returns routes already filtered by the user's JWT/session.
|
|
6
|
+
|
|
7
|
+
## 5 Mandatory Elements
|
|
8
|
+
|
|
9
|
+
### 1. componentRegistry
|
|
10
|
+
|
|
11
|
+
**Role**: Static mapping `string -> lazy(component)`. The ONLY allowed static file.
|
|
12
|
+
|
|
13
|
+
**Requirements**:
|
|
14
|
+
- All page imports MUST be lazy (`React.lazy(() => import(...))`)
|
|
15
|
+
- Keys are string identifiers (dot-separated hierarchical recommended)
|
|
16
|
+
- No business logic in the registry — pure mapping only
|
|
17
|
+
- Must be loaded at app startup (imported in main.tsx or App.tsx)
|
|
18
|
+
- New pages are registered here and ONLY here
|
|
19
|
+
|
|
20
|
+
**SmartStack specifics**:
|
|
21
|
+
- File: `componentRegistry.generated.ts`
|
|
22
|
+
- Registration via `PageRegistry.register()` (v3.7+)
|
|
23
|
+
- `PAGE_KEYS` constant for type-safe key references
|
|
24
|
+
- Generated by MCP `scaffold_routes` tool
|
|
25
|
+
|
|
26
|
+
### 2. ProtectedRoute
|
|
27
|
+
|
|
28
|
+
**Role**: Front-end guard that validates permissions received from the API (defense in depth).
|
|
29
|
+
|
|
30
|
+
**Requirements**:
|
|
31
|
+
- Receives permission requirements from route config (NOT hardcoded)
|
|
32
|
+
- Checks user permissions against route requirements
|
|
33
|
+
- Redirects unauthorized users (to login or forbidden page)
|
|
34
|
+
- Does NOT contain a list of routes or paths
|
|
35
|
+
- Permissions come from the API, not from static constants
|
|
36
|
+
|
|
37
|
+
**SmartStack specifics**:
|
|
38
|
+
- File: `ProtectedRoute.tsx`
|
|
39
|
+
- Replaced the old `RouteGuard.tsx` (which had hardcoded lists)
|
|
40
|
+
- Uses `permissionPath` from menu API DTOs
|
|
41
|
+
|
|
42
|
+
### 3. DynamicRouter
|
|
43
|
+
|
|
44
|
+
**Role**: Rendering engine that generates `<Route>` elements from the API configuration.
|
|
45
|
+
|
|
46
|
+
**Requirements**:
|
|
47
|
+
- Iterates over API route config to create React Router `<Route>` elements
|
|
48
|
+
- Resolves components from componentRegistry by key
|
|
49
|
+
- Wraps routes with ProtectedRoute for permission checking
|
|
50
|
+
- Handles nested routes (outlets, tabs)
|
|
51
|
+
- Supports loading states (shows loader while config is being fetched)
|
|
52
|
+
- No hardcoded route paths in the router itself
|
|
53
|
+
|
|
54
|
+
**SmartStack specifics**:
|
|
55
|
+
- File: `DynamicRouter.tsx`
|
|
56
|
+
- Sole routing engine — App.tsx reduced to providers + DynamicRouter
|
|
57
|
+
- Handles: dynamic app routes, feature gating, auto-redirects, outlet tab routes, doc routes
|
|
58
|
+
- `OUTLET_SECTIONS` config for tab-based pages
|
|
59
|
+
- `getStaticAppRoutes()` for per-app legacy redirects (migration period only)
|
|
60
|
+
|
|
61
|
+
### 4. useRouteConfig
|
|
62
|
+
|
|
63
|
+
**Role**: Hook that fetches route configuration + permissions from the API at mount.
|
|
64
|
+
|
|
65
|
+
**Requirements**:
|
|
66
|
+
- Calls navigation API on mount (or auth change)
|
|
67
|
+
- Returns structured route config with components, paths, and permissions
|
|
68
|
+
- Provides loading/error states
|
|
69
|
+
- Caches appropriately (invalidates on auth change)
|
|
70
|
+
- Flattens hierarchical menu into usable route entries
|
|
71
|
+
|
|
72
|
+
**SmartStack specifics**:
|
|
73
|
+
- File: `useRouteConfig.ts`
|
|
74
|
+
- Flattens menu API response into `RouteConfigEntry[]`
|
|
75
|
+
- Provides `byKey` and `byApplication` maps
|
|
76
|
+
- Used by DynamicRouter to build routes
|
|
77
|
+
|
|
78
|
+
### 5. Navigation API
|
|
79
|
+
|
|
80
|
+
**Role**: Endpoint returning routes already filtered by JWT/session.
|
|
81
|
+
|
|
82
|
+
**Requirements**:
|
|
83
|
+
- Returns routes with: path, componentKey, permissions, children
|
|
84
|
+
- Filters routes based on authenticated user's permissions
|
|
85
|
+
- Hierarchical structure (Application > Module > Section > Resource)
|
|
86
|
+
- Includes permission paths for front-end guard
|
|
87
|
+
- Supports feature gating (license-based)
|
|
88
|
+
|
|
89
|
+
**SmartStack specifics**:
|
|
90
|
+
- Endpoint: `GET /api/navigation/menu`
|
|
91
|
+
- Returns enriched DTOs at 4 levels (Application, Module, Section, Resource)
|
|
92
|
+
- `ComponentKey` and `PermissionPath` on all 4 DTO levels
|
|
93
|
+
- `RequiredFeature` for license-based feature gating
|
|
94
|
+
- Built by `NavigationService.BuildApplicationMenu/BuildModuleMenu/BuildSectionMenu/BuildResourceMenu`
|
|
95
|
+
|
|
96
|
+
## Expected API Response Structure
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"routes": [
|
|
101
|
+
{
|
|
102
|
+
"path": "/administration/users",
|
|
103
|
+
"componentKey": "administration.users.list",
|
|
104
|
+
"permissionPath": "administration.users",
|
|
105
|
+
"permissions": ["administration.users.access", "administration.users.read"],
|
|
106
|
+
"children": [
|
|
107
|
+
{
|
|
108
|
+
"path": ":id",
|
|
109
|
+
"componentKey": "administration.users.detail",
|
|
110
|
+
"permissions": ["administration.users.read"]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Common Anti-Patterns
|
|
119
|
+
|
|
120
|
+
| Anti-Pattern | Description | Risk |
|
|
121
|
+
|-------------|-------------|------|
|
|
122
|
+
| Hardcoded routes in JSX | `<Route path="/admin" ...>` outside DynamicRouter | Routes drift from DB, permissions bypassed |
|
|
123
|
+
| Static permission lists | `const ADMIN_PERMS = [...]` in components | Permissions out of sync with API |
|
|
124
|
+
| Non-lazy imports in registry | `import Page from './Page'` (eager) | Bundle bloat, slow initial load |
|
|
125
|
+
| Missing Suspense boundary | No `<Suspense>` around lazy components | Crashes on slow loads |
|
|
126
|
+
| Route paths in components | `navigate('/admin/users')` with hardcoded paths | Tight coupling, breaks if DB paths change |
|
|
127
|
+
| Permission checks without API | `if (user.role === 'admin')` | Bypasses permission system |
|
|
128
|
+
| Duplicate route definitions | Routes in both DynamicRouter and static config | Conflicts, unpredictable behavior |
|
|
129
|
+
| Missing loading state | No loader while route config is fetching | Flash of "not found" page |
|