@atlashub/smartstack-cli 4.35.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 (46) hide show
  1. package/dist/index.js +28 -32
  2. package/dist/index.js.map +1 -1
  3. package/dist/mcp-entry.mjs +29 -10
  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 +294 -2
  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/dev-start/SKILL.md +12 -2
  19. package/templates/skills/efcore/SKILL.md +219 -67
  20. package/templates/agents/efcore/conflicts.md +0 -114
  21. package/templates/agents/efcore/db-deploy.md +0 -86
  22. package/templates/agents/efcore/db-reset.md +0 -98
  23. package/templates/agents/efcore/db-seed.md +0 -73
  24. package/templates/agents/efcore/db-status.md +0 -97
  25. package/templates/agents/efcore/scan.md +0 -124
  26. package/templates/skills/efcore/references/both-contexts.md +0 -32
  27. package/templates/skills/efcore/references/destructive-operations.md +0 -38
  28. package/templates/skills/efcore/steps/db/step-deploy.md +0 -217
  29. package/templates/skills/efcore/steps/db/step-reset.md +0 -186
  30. package/templates/skills/efcore/steps/db/step-seed.md +0 -166
  31. package/templates/skills/efcore/steps/db/step-status.md +0 -173
  32. package/templates/skills/efcore/steps/migration/step-00-init.md +0 -102
  33. package/templates/skills/efcore/steps/migration/step-01-check.md +0 -164
  34. package/templates/skills/efcore/steps/migration/step-02-create.md +0 -160
  35. package/templates/skills/efcore/steps/migration/step-03-validate.md +0 -168
  36. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +0 -173
  37. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +0 -100
  38. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +0 -115
  39. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +0 -112
  40. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +0 -157
  41. package/templates/skills/efcore/steps/shared/step-00-init.md +0 -131
  42. package/templates/skills/efcore/steps/squash/step-00-init.md +0 -141
  43. package/templates/skills/efcore/steps/squash/step-01-backup.md +0 -120
  44. package/templates/skills/efcore/steps/squash/step-02-fetch.md +0 -168
  45. package/templates/skills/efcore/steps/squash/step-03-create.md +0 -184
  46. package/templates/skills/efcore/steps/squash/step-04-validate.md +0 -174
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlashub/smartstack-cli",
3
- "version": "4.35.0",
3
+ "version": "4.36.0",
4
4
  "description": "SmartStack Claude Code automation toolkit - GitFlow, EF Core migrations, prompts and more",
5
5
  "author": {
6
6
  "name": "SmartStack",
@@ -249,7 +249,7 @@ if [ -n "$PROVIDER" ]; then
249
249
  echo "CRITICAL: Translation seed data inserts without idempotency guard in $PROVIDER"
250
250
  echo "Fix: Before each NavigationTranslations.Add block, check existence:"
251
251
  echo " if (!await context.NavigationTranslations.AnyAsync("
252
- echo " t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId"
252
+ echo " t => t.EntityId == mod1Entity.Id // actual DB ID, NOT seed-time GUID"
253
253
  echo " && t.EntityType == NavigationEntityType.Module, ct))"
254
254
  echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
255
255
  echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
@@ -109,6 +109,7 @@ public static class NavigationApplicationSeedData
109
109
  return new NavigationApplicationSeedEntry
110
110
  {
111
111
  Id = ApplicationId,
112
+ Zone = ApplicationZone.Business,
112
113
  Code = "{appCode}",
113
114
  Label = "{appLabel_en}",
114
115
  Description = "{appDesc_en}",
@@ -198,6 +199,7 @@ public static class NavigationApplicationSeedData
198
199
  public class NavigationApplicationSeedEntry
199
200
  {
200
201
  public Guid Id { get; init; }
202
+ public ApplicationZone Zone { get; init; }
201
203
  public string Code { get; init; } = null!;
202
204
  public string Label { get; init; } = null!;
203
205
  public string? Description { get; init; }
@@ -1007,11 +1009,12 @@ public class RolePermissionSeedEntry
1007
1009
 
1008
1010
  | Rule | Description |
1009
1011
  |------|-------------|
1010
- | Factory methods | `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)`do not use `new Entity()` |
1012
+ | Factory methods | `NavigationApplication.Create(zone, ...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)` — NEVER `new Entity()` |
1011
1013
  | Idempotence | Each Seed method checks existence before inserting |
1012
1014
  | Execution order | Navigation → Roles → Permissions → RolePermissions (roles MUST exist before mapping) |
1013
1015
  | SaveChanges per group | Navigation -> save -> Roles -> save -> Permissions -> save -> RolePermissions -> save |
1014
- | FK resolution by Code | Parent entities (modules, roles) found by `Code`, not hardcoded GUID |
1016
+ | FK resolution by Code | Parent entities (applications, modules, roles) found by `Code` from DB, not seed-time GUID |
1017
+ | Translation EntityId | ALWAYS use actual DB ID (`app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes |
1015
1018
  | DI registration | `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()` |
1016
1019
 
1017
1020
  ### Template
@@ -1054,7 +1057,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1054
1057
  else
1055
1058
  {
1056
1059
  app = NavigationApplication.Create(
1057
- appEntry.Code, appEntry.Label,
1060
+ appEntry.Zone, appEntry.Code, appEntry.Label,
1058
1061
  appEntry.Description, appEntry.Icon, appEntry.IconType,
1059
1062
  appEntry.Route, appEntry.DisplayOrder);
1060
1063
  context.NavigationApplications.Add(app);
@@ -1064,7 +1067,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1064
1067
  foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
1065
1068
  {
1066
1069
  context.NavigationTranslations.Add(
1067
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1070
+ NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
1068
1071
  }
1069
1072
  await ((DbContext)context).SaveChangesAsync(ct);
1070
1073
  }
@@ -1097,14 +1100,15 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1097
1100
  // --- Module translations (idempotent — unique index IX_nav_Translations_EntityType_EntityId_LanguageCode) ---
1098
1101
  // Always check existence before inserting translations to avoid duplicate key errors
1099
1102
  // on re-runs, partial failures, or DB reset scenarios.
1103
+ // CRITICAL: Use mod1Entity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
1100
1104
  if (!await context.NavigationTranslations.AnyAsync(
1101
- t => t.EntityId == {Module1Pascal}NavigationSeedData.{Module1Pascal}ModuleId
1105
+ t => t.EntityId == mod1Entity.Id
1102
1106
  && t.EntityType == NavigationEntityType.Module, ct))
1103
1107
  {
1104
1108
  foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
1105
1109
  {
1106
1110
  context.NavigationTranslations.Add(
1107
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1111
+ NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description));
1108
1112
  }
1109
1113
  }
1110
1114
  // Repeat for each module...
@@ -1129,16 +1133,21 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1129
1133
  await ((DbContext)context).SaveChangesAsync(ct);
1130
1134
 
1131
1135
  // --- Section translations (idempotent — check before inserting) ---
1136
+ // CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
1137
+ // Resolve actual section from DB by Code+ModuleId to get the real ID.
1132
1138
  foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
1133
1139
  {
1140
+ var actualSection = await context.NavigationSections
1141
+ .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
1142
+
1134
1143
  if (!await context.NavigationTranslations.AnyAsync(
1135
- t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
1144
+ t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
1136
1145
  {
1137
1146
  foreach (var t in {Module1Pascal}NavigationSeedData.GetSectionTranslationEntries()
1138
1147
  .Where(st => st.EntityId == secEntry.Id))
1139
1148
  {
1140
1149
  context.NavigationTranslations.Add(
1141
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
1150
+ NavigationTranslation.Create(t.EntityType, actualSection.Id, t.LanguageCode, t.Label, t.Description));
1142
1151
  }
1143
1152
  }
1144
1153
  }
@@ -1174,10 +1183,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1174
1183
 
1175
1184
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
1176
1185
  {
1177
- // Check idempotence verify by Code (roles may already exist from SmartStack core)
1178
- // Scope by ApplicationId — without this, roles from OTHER apps cause false positives
1186
+ // Resolve application from DB by Code NOT seed-time GUID
1187
+ var app = await context.NavigationApplications
1188
+ .FirstAsync(a => a.Code == "{appCode}", ct);
1189
+
1190
+ // Check idempotence — verify by Code, scoped by actual DB ApplicationId
1179
1191
  var existingRoleCodes = await context.Roles
1180
- .Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId
1192
+ .Where(r => r.ApplicationId == app.Id
1181
1193
  && (r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer"))
1182
1194
  .Select(r => r.Code)
1183
1195
  .ToListAsync(ct);
@@ -1188,11 +1200,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1188
1200
  if (existingRoleCodes.Contains(entry.Code)) continue; // Skip if already exists
1189
1201
 
1190
1202
  var role = Role.Create(
1191
- entry.Code,
1192
- entry.Name,
1193
- entry.Description,
1194
- entry.ApplicationId,
1195
- entry.IsSystem);
1203
+ name: entry.Name,
1204
+ shortName: entry.Code,
1205
+ category: RoleCategory.Application,
1206
+ description: entry.Description,
1207
+ isSystem: entry.IsSystem,
1208
+ applicationId: app.Id,
1209
+ code: entry.Code);
1196
1210
  context.Roles.Add(role);
1197
1211
  }
1198
1212
  await ((DbContext)context).SaveChangesAsync(ct);
@@ -1231,13 +1245,17 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
1231
1245
  .AnyAsync(rp => rp.Permission!.Path.StartsWith("{appCode}."), ct);
1232
1246
  if (exists) return;
1233
1247
 
1248
+ // Resolve application from DB by Code — NOT seed-time GUID
1249
+ var app = await context.NavigationApplications
1250
+ .FirstAsync(a => a.Code == "{appCode}", ct);
1251
+
1234
1252
  // Resolve roles by Code from DB — do not use hardcoded GUIDs.
1235
1253
  // Application-scoped roles (admin, manager, contributor, viewer) are created by
1236
1254
  // SeedRolesAsync() above. System roles use their own IDs that do not match
1237
1255
  // DeterministicGuid("role:admin").
1238
1256
  // Load THIS application's roles + system roles only
1239
1257
  var roles = await context.Roles
1240
- .Where(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId || r.IsSystem)
1258
+ .Where(r => r.ApplicationId == app.Id || r.IsSystem)
1241
1259
  .ToListAsync(ct);
1242
1260
 
1243
1261
  // Resolve permissions
@@ -1333,11 +1351,11 @@ Before marking the task as completed, verify ALL:
1333
1351
 
1334
1352
  **Application-Level (FIRST — before modules):**
1335
1353
  - [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
1336
- - [ ] Application GUID is deterministic (SHA256 of `"navigation-application-{appCode}"`)
1337
- - [ ] GetApplicationEntry() takes no parameters (no contextId)
1338
- - [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application)
1354
+ - [ ] Application GUID is random (`Guid.NewGuid()`) — FK resolution is by Code lookup, not fixed ID
1355
+ - [ ] GetApplicationEntry() takes no parameters (no contextId), includes `Zone = ApplicationZone.Business`
1356
+ - [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application), using `app.Id` (actual DB ID) for EntityId
1339
1357
  - [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
1340
- - [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (NO `{ApplicationGuid}` placeholder)
1358
+ - [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (DTO only — provider code resolves from DB by Code)
1341
1359
 
1342
1360
  **Module-Level:**
1343
1361
  - [ ] Random GUIDs via `Guid.NewGuid()` (no deterministic/sequential/fixed values)
@@ -147,20 +147,26 @@ Add a new method `SeedRolesAsync()` to the provider:
147
147
  ```csharp
148
148
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
149
149
  {
150
- // Check idempotence
150
+ // Resolve application from DB by Code — NOT seed-time GUID
151
+ var app = await context.NavigationApplications
152
+ .FirstAsync(a => a.Code == "{app_code}", ct);
153
+
154
+ // Check idempotence — scoped by actual DB ApplicationId
151
155
  var exists = await context.Roles
152
- .AnyAsync(r => r.ApplicationId == ApplicationRolesSeedData.ApplicationId, ct);
156
+ .AnyAsync(r => r.ApplicationId == app.Id, ct);
153
157
  if (exists) return;
154
158
 
155
159
  // Create application-scoped roles using factory method
156
160
  foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
157
161
  {
158
162
  var role = Role.Create(
159
- entry.Code,
160
- entry.Name,
161
- entry.Description,
162
- entry.ApplicationId,
163
- entry.IsSystem);
163
+ name: entry.Name,
164
+ shortName: entry.Code,
165
+ category: RoleCategory.Application,
166
+ description: entry.Description,
167
+ isSystem: entry.IsSystem,
168
+ applicationId: app.Id,
169
+ code: entry.Code);
164
170
 
165
171
  context.Roles.Add(role);
166
172
  }
@@ -205,7 +211,7 @@ Before marking the task as completed, verify:
205
211
  ## Notes
206
212
 
207
213
  - **Application ID source:** Read from the navigation application created in `SeedNavigationAsync()` or from `{AppPascal}NavigationSeedData.cs`
208
- - **Role factory method:** Use `Role.Create(code, name, description, applicationId, isSystem)` from SmartStack.Domain
214
+ - **Role factory method:** Use `Role.Create(name, shortName, category, description, isSystem, applicationId: ..., code: ...)` from SmartStack.Domain
209
215
  - **Code uniqueness:** Role codes must be unique within the application scope
210
216
  - **System roles:** These are NOT system roles (IsSystem = false) - they are application-scoped roles
211
217
  - **Tenant isolation:** Application-scoped roles are automatically tenant-isolated via the Core authorization system
@@ -43,7 +43,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
43
43
  else
44
44
  {
45
45
  app = NavigationApplication.Create(
46
- appEntry.Code, appEntry.Label,
46
+ ApplicationZone.Business, appEntry.Code, appEntry.Label,
47
47
  appEntry.Description, appEntry.Icon, appEntry.IconType,
48
48
  appEntry.Route, appEntry.DisplayOrder);
49
49
  context.NavigationApplications.Add(app);
@@ -53,7 +53,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
53
53
  foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
54
54
  {
55
55
  context.NavigationTranslations.Add(
56
- NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
56
+ NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
57
57
  }
58
58
  await ((DbContext)context).SaveChangesAsync(ct);
59
59
  }
@@ -72,18 +72,24 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
72
72
  // --- Module translations (IDEMPOTENT — check before inserting) ---
73
73
  // CRITICAL: nav_Translations has unique index IX_nav_Translations_EntityType_EntityId_LanguageCode
74
74
  // Always guard with AnyAsync to prevent duplicate key errors on re-runs.
75
+ // CRITICAL: Use modEntity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
75
76
  // if (!await context.NavigationTranslations.AnyAsync(
76
- // t => t.EntityId == {Module}NavigationSeedData.{Module}ModuleId
77
+ // t => t.EntityId == mod1Entity.Id
77
78
  // && t.EntityType == NavigationEntityType.Module, ct))
78
- // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries()) { ... } }
79
+ // { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
80
+ // { context.NavigationTranslations.Add(
81
+ // NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description)); } }
79
82
  await ((DbContext)context).SaveChangesAsync(ct);
80
83
 
81
84
  // --- Sections (idempotent per-section) ---
82
85
  // Check each section before inserting: AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == ...)
83
86
 
84
87
  // --- Section translations (IDEMPOTENT — same guard pattern as module translations) ---
88
+ // CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
89
+ // Resolve actual section from DB: var actualSection = await context.NavigationSections
90
+ // .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
85
91
  // if (!await context.NavigationTranslations.AnyAsync(
86
- // t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
92
+ // t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
87
93
 
88
94
  // --- Resources (idempotent — use ACTUAL section IDs from DB) ---
89
95
  // CRITICAL: NavigationSection.Create() generates its own random ID.
@@ -95,10 +101,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
95
101
 
96
102
  public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
97
103
  {
98
- // Check idempotence
99
- var applicationId = ApplicationRolesSeedData.ApplicationId;
104
+ // Resolve application from DB by Code — NOT seed-time GUID
105
+ var app = await context.NavigationApplications
106
+ .FirstAsync(a => a.Code == "{app_code}", ct);
107
+
108
+ // Check idempotence — scoped by actual DB ApplicationId
100
109
  var exists = await context.Roles
101
- .AnyAsync(r => r.ApplicationId == applicationId, ct);
110
+ .AnyAsync(r => r.ApplicationId == app.Id, ct);
102
111
  if (exists) return;
103
112
 
104
113
  // Create application-scoped roles (Admin, Manager, Contributor, Viewer)
@@ -106,11 +115,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
106
115
  foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
107
116
  {
108
117
  var role = Role.Create(
109
- entry.Code,
110
- entry.Name,
111
- entry.Description,
112
- entry.ApplicationId,
113
- entry.IsSystem);
118
+ name: entry.Name,
119
+ shortName: entry.Code,
120
+ category: RoleCategory.Application,
121
+ description: entry.Description,
122
+ isSystem: entry.IsSystem,
123
+ applicationId: app.Id,
124
+ code: entry.Code);
114
125
  context.Roles.Add(role);
115
126
  }
116
127
  await ((DbContext)context).SaveChangesAsync(ct);
@@ -167,11 +178,12 @@ services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
167
178
 
168
179
  ## Critical Rules
169
180
 
170
- 1. **Factory methods mandatory**: `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
181
+ 1. **Factory methods mandatory**: `NavigationApplication.Create(zone, ...)`, `NavigationModule.Create(...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
171
182
  2. **Idempotence**: Each Seed method checks existence before inserting. **Translations require explicit `AnyAsync` guard** — unique index `IX_nav_Translations_EntityType_EntityId_LanguageCode` causes crashes on duplicate inserts
172
- 3. **Translation guard pattern**: `if (!await context.NavigationTranslations.AnyAsync(t => t.EntityId == {id} && t.EntityType == ..., ct))` MANDATORY before every translation batch insert
173
- 4. **SaveChangesAsync per group**: Navigation save Roles save Permissions save RolePermissions save
174
- 4. **Execution order**: `SeedRolesAsync()` MUST be called BEFORE `SeedRolePermissionsAsync()` (roles must exist before mapping)
175
- 5. **Random GUIDs**: ALWAYS use `Guid.NewGuid()` resolve FKs by Code lookup, not fixed IDs
176
- 6. **Resolve FK by Code**: Parent modules and roles are found by `Code`, not hardcoded GUID
177
- 7. **Order property**: Use `100` as default. If multiple providers exist, they run in Order sequence
183
+ 3. **Translation EntityId**: ALWAYS use actual DB ID (e.g. `app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes
184
+ 4. **Translation guard pattern**: `if (!await context.NavigationTranslations.AnyAsync(t => t.EntityId == {actualDbId} && t.EntityType == ..., ct))` MANDATORY before every translation batch insert
185
+ 5. **SaveChangesAsync per group**: Navigation save Roles save Permissions save → RolePermissions → save
186
+ 6. **Execution order**: `SeedRolesAsync()` MUST be called BEFORE `SeedRolePermissionsAsync()` (roles must exist before mapping)
187
+ 7. **Random GUIDs**: ALWAYS use `Guid.NewGuid()` resolve FKs by Code lookup, not fixed IDs
188
+ 8. **Resolve FK by Code**: Parent entities (applications, modules, roles) are found by `Code` from DB, not by seed-time GUID
189
+ 9. **Order property**: Use `100` as default. If multiple providers exist, they run in Order sequence
@@ -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
- | ☐ Routes added inside Layout wrapper in App.tsx | |
625
- | ☐ 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 }`) | |
626
918
 
627
919
  ### Theme Compliance
628
920
  | Check | Status |