@nsxbet/admin-sdk 0.5.1 → 0.6.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 (173) hide show
  1. package/CHECKLIST.md +40 -10
  2. package/README.md +337 -36
  3. package/dist/auth/client/gateway-token.d.ts +19 -0
  4. package/dist/auth/client/gateway-token.js +89 -0
  5. package/dist/auth/client/in-memory.d.ts +5 -1
  6. package/dist/auth/client/in-memory.js +75 -38
  7. package/dist/auth/client/index.d.ts +0 -1
  8. package/dist/auth/client/interface.d.ts +6 -3
  9. package/dist/auth/client/keycloak.d.ts +0 -1
  10. package/dist/auth/client/keycloak.js +6 -3
  11. package/dist/auth/components/UserSelector.d.ts +0 -1
  12. package/dist/auth/components/UserSelector.js +89 -7
  13. package/dist/auth/components/index.d.ts +0 -1
  14. package/dist/auth/index.d.ts +0 -1
  15. package/dist/components/AuthProvider.d.ts +0 -1
  16. package/dist/components/Timestamp.d.ts +7 -0
  17. package/dist/components/Timestamp.js +50 -0
  18. package/dist/hooks/useAuth.d.ts +0 -1
  19. package/dist/hooks/useAuth.js +1 -1
  20. package/dist/hooks/useFetch.d.ts +0 -1
  21. package/dist/hooks/useI18n.d.ts +0 -1
  22. package/dist/hooks/usePlatformAPI.d.ts +0 -1
  23. package/dist/hooks/useTelemetry.d.ts +0 -1
  24. package/dist/hooks/useTimestamp.d.ts +8 -0
  25. package/dist/hooks/useTimestamp.js +122 -0
  26. package/dist/i18n/config.d.ts +20 -2
  27. package/dist/i18n/config.js +48 -0
  28. package/dist/i18n/index.d.ts +2 -3
  29. package/dist/i18n/index.js +1 -1
  30. package/dist/i18n/locales/en-US.json +95 -18
  31. package/dist/i18n/locales/es.json +95 -18
  32. package/dist/i18n/locales/pt-BR.json +95 -18
  33. package/dist/i18n/locales/ro.json +95 -18
  34. package/dist/index.d.ts +11 -7
  35. package/dist/index.js +5 -1
  36. package/dist/registry/AdminShellRegistry.d.ts +1 -2
  37. package/dist/registry/cache/cached-catalog.d.ts +11 -0
  38. package/dist/registry/cache/cached-catalog.js +42 -0
  39. package/dist/registry/cache/catalog-cache.d.ts +10 -0
  40. package/dist/registry/cache/catalog-cache.js +58 -0
  41. package/dist/registry/cache/index.d.ts +5 -0
  42. package/dist/registry/cache/index.js +3 -0
  43. package/dist/registry/cache/types.d.ts +20 -0
  44. package/dist/registry/cache/types.js +3 -0
  45. package/dist/registry/client/http.d.ts +0 -1
  46. package/dist/registry/client/http.js +13 -0
  47. package/dist/registry/client/in-memory.d.ts +0 -1
  48. package/dist/registry/client/in-memory.js +117 -12
  49. package/dist/registry/client/index.d.ts +0 -1
  50. package/dist/registry/client/interface.d.ts +21 -6
  51. package/dist/registry/index.d.ts +5 -2
  52. package/dist/registry/index.js +4 -0
  53. package/dist/registry/types/index.d.ts +2 -3
  54. package/dist/registry/types/manifest.d.ts +20 -24
  55. package/dist/registry/types/manifest.js +17 -18
  56. package/dist/registry/types/module.d.ts +43 -14
  57. package/dist/registry/useRegistryPolling.d.ts +15 -0
  58. package/dist/registry/useRegistryPolling.js +66 -0
  59. package/dist/router/DynamicModule.d.ts +6 -22
  60. package/dist/router/DynamicModule.js +25 -48
  61. package/dist/router/ModuleErrorBoundary.d.ts +39 -0
  62. package/dist/router/ModuleErrorBoundary.js +101 -0
  63. package/dist/router/index.d.ts +1 -1
  64. package/dist/router/url-allowlist.d.ts +22 -0
  65. package/dist/router/url-allowlist.js +65 -0
  66. package/dist/shell/AdminShell.d.ts +0 -1
  67. package/dist/shell/AdminShell.js +178 -43
  68. package/dist/shell/BackofficeShell.d.ts +0 -1
  69. package/dist/shell/BackofficeShell.js +59 -25
  70. package/dist/shell/components/CommandPalette.d.ts +0 -1
  71. package/dist/shell/components/CommandPalette.js +26 -50
  72. package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
  73. package/dist/shell/components/DevtoolsPanel.js +145 -0
  74. package/dist/shell/components/HomePage.d.ts +0 -1
  75. package/dist/shell/components/HomePage.js +9 -4
  76. package/dist/shell/components/LeftNav.d.ts +0 -1
  77. package/dist/shell/components/LeftNav.js +91 -93
  78. package/dist/shell/components/MainContent.d.ts +3 -2
  79. package/dist/shell/components/MainContent.js +8 -23
  80. package/dist/shell/components/ModuleOverview.d.ts +0 -1
  81. package/dist/shell/components/ModuleOverview.js +4 -20
  82. package/dist/shell/components/ProfilePage.d.ts +0 -1
  83. package/dist/shell/components/ProfilePage.js +1 -1
  84. package/dist/shell/components/RegistryPage.d.ts +0 -1
  85. package/dist/shell/components/RegistryPage.js +154 -64
  86. package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
  87. package/dist/shell/components/RegistryStatusBanner.js +31 -0
  88. package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
  89. package/dist/shell/components/RegistryUnavailable.js +7 -0
  90. package/dist/shell/components/SettingsPage.d.ts +0 -1
  91. package/dist/shell/components/StackedPanel.d.ts +15 -0
  92. package/dist/shell/components/StackedPanel.js +45 -0
  93. package/dist/shell/components/TopBar.d.ts +4 -2
  94. package/dist/shell/components/TopBar.js +9 -3
  95. package/dist/shell/components/UpdateBanner.d.ts +5 -0
  96. package/dist/shell/components/UpdateBanner.js +8 -0
  97. package/dist/shell/components/index.d.ts +4 -1
  98. package/dist/shell/components/index.js +2 -0
  99. package/dist/shell/components/theme-provider.d.ts +0 -1
  100. package/dist/shell/components/theme-provider.js +8 -5
  101. package/dist/shell/hooks/useCspViolations.d.ts +12 -0
  102. package/dist/shell/hooks/useCspViolations.js +34 -0
  103. package/dist/shell/index.d.ts +1 -2
  104. package/dist/shell/polling-config.d.ts +10 -0
  105. package/dist/shell/polling-config.js +26 -0
  106. package/dist/shell/search/fuzzy.d.ts +0 -1
  107. package/dist/shell/search/index.d.ts +0 -1
  108. package/dist/shell/telemetry.d.ts +0 -1
  109. package/dist/shell/types.d.ts +34 -18
  110. package/dist/tailwind/index.d.ts +0 -1
  111. package/dist/types/keycloak.d.ts +0 -1
  112. package/dist/types/platform.d.ts +12 -1
  113. package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
  114. package/dist/vite/AdminShellSharedDeps.js +215 -0
  115. package/dist/vite/config.d.ts +2 -2
  116. package/dist/vite/config.js +5 -7
  117. package/dist/vite/i18n-plugin.d.ts +13 -0
  118. package/dist/vite/i18n-plugin.js +81 -0
  119. package/dist/vite/index.d.ts +2 -1
  120. package/dist/vite/index.js +2 -0
  121. package/dist/vite/plugins.d.ts +0 -1
  122. package/package.json +6 -2
  123. package/dist/auth/client/in-memory.d.ts.map +0 -1
  124. package/dist/auth/client/index.d.ts.map +0 -1
  125. package/dist/auth/client/interface.d.ts.map +0 -1
  126. package/dist/auth/client/keycloak.d.ts.map +0 -1
  127. package/dist/auth/components/UserSelector.d.ts.map +0 -1
  128. package/dist/auth/components/index.d.ts.map +0 -1
  129. package/dist/auth/index.d.ts.map +0 -1
  130. package/dist/components/AuthProvider.d.ts.map +0 -1
  131. package/dist/hooks/useAuth.d.ts.map +0 -1
  132. package/dist/hooks/useFetch.d.ts.map +0 -1
  133. package/dist/hooks/useI18n.d.ts.map +0 -1
  134. package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
  135. package/dist/hooks/useTelemetry.d.ts.map +0 -1
  136. package/dist/i18n/config.d.ts.map +0 -1
  137. package/dist/i18n/index.d.ts.map +0 -1
  138. package/dist/index.d.ts.map +0 -1
  139. package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
  140. package/dist/registry/client/http.d.ts.map +0 -1
  141. package/dist/registry/client/in-memory.d.ts.map +0 -1
  142. package/dist/registry/client/index.d.ts.map +0 -1
  143. package/dist/registry/client/interface.d.ts.map +0 -1
  144. package/dist/registry/index.d.ts.map +0 -1
  145. package/dist/registry/types/index.d.ts.map +0 -1
  146. package/dist/registry/types/manifest.d.ts.map +0 -1
  147. package/dist/registry/types/module.d.ts.map +0 -1
  148. package/dist/router/DynamicModule.d.ts.map +0 -1
  149. package/dist/router/index.d.ts.map +0 -1
  150. package/dist/shell/AdminShell.d.ts.map +0 -1
  151. package/dist/shell/BackofficeShell.d.ts.map +0 -1
  152. package/dist/shell/components/CommandPalette.d.ts.map +0 -1
  153. package/dist/shell/components/HomePage.d.ts.map +0 -1
  154. package/dist/shell/components/LeftNav.d.ts.map +0 -1
  155. package/dist/shell/components/MainContent.d.ts.map +0 -1
  156. package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
  157. package/dist/shell/components/ProfilePage.d.ts.map +0 -1
  158. package/dist/shell/components/RegistryPage.d.ts.map +0 -1
  159. package/dist/shell/components/SettingsPage.d.ts.map +0 -1
  160. package/dist/shell/components/TopBar.d.ts.map +0 -1
  161. package/dist/shell/components/index.d.ts.map +0 -1
  162. package/dist/shell/components/theme-provider.d.ts.map +0 -1
  163. package/dist/shell/index.d.ts.map +0 -1
  164. package/dist/shell/search/fuzzy.d.ts.map +0 -1
  165. package/dist/shell/search/index.d.ts.map +0 -1
  166. package/dist/shell/telemetry.d.ts.map +0 -1
  167. package/dist/shell/types.d.ts.map +0 -1
  168. package/dist/tailwind/index.d.ts.map +0 -1
  169. package/dist/types/keycloak.d.ts.map +0 -1
  170. package/dist/types/platform.d.ts.map +0 -1
  171. package/dist/vite/config.d.ts.map +0 -1
  172. package/dist/vite/index.d.ts.map +0 -1
  173. package/dist/vite/plugins.d.ts.map +0 -1
package/CHECKLIST.md CHANGED
@@ -12,7 +12,7 @@
12
12
  - [ ] `vite.config.ts` — Vite build config
13
13
  - [ ] `index.html` — HTML entry point
14
14
  - [ ] `src/spa.tsx` — Shell entry (default export)
15
- - [ ] `src/standalone.tsx` — Dev entry (AdminShell wrapper)
15
+ - [ ] `src/main.tsx` — Dev entry (AdminShell wrapper)
16
16
  - [ ] `src/App.tsx` — Main app component (Routes)
17
17
  - [ ] `src/index.css` — Styles with Tailwind directives
18
18
  - [ ] `tailwind.config.js` — Tailwind config
@@ -20,17 +20,36 @@
20
20
  - [ ] `tsconfig.json` — TypeScript config
21
21
  - [ ] `tsconfig.node.json` — Node TypeScript config
22
22
  - [ ] `src/globals.d.ts` — Global type declarations
23
+ - [ ] `src/i18n/locales/en-US.json` — English (US) translations
24
+ - [ ] `src/i18n/locales/pt-BR.json` — Portuguese (Brazil) translations
25
+ - [ ] `src/i18n/locales/es.json` — Spanish translations
26
+ - [ ] `src/i18n/locales/ro.json` — Romanian translations
27
+ - [ ] `.env.staging` — Gateway environment config (staging mode)
28
+
29
+ ## Environment Configuration
30
+
31
+ - [ ] `.env.staging` exists with `VITE_ADMIN_GATEWAY_URL` configured
32
+ - [ ] `dev` script uses `--mode staging` to load `.env.staging` by default
33
+
34
+ Expected `.env.staging`:
35
+
36
+ ```bash
37
+ # Gateway URL for BFF token integration (InMemory auth)
38
+ # Fetches real signed JWTs from the admin gateway instead of mock tokens.
39
+ # Staging: https://admin-bff-stg.nsx.dev | Homol: https://admin-bff-homol.nsx.dev | Local: http://localhost:8080
40
+ VITE_ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
41
+ ```
23
42
 
24
43
  ## Module Manifest (`admin.module.json`)
25
44
 
26
45
  - [ ] Has `id` field (format: `@admin/<name>`)
27
- - [ ] Has `title` field
46
+ - [ ] Has `title` field (localized object with en-US, pt-BR, es, ro)
28
47
  - [ ] Has `routeBase` field (starts with `/`)
29
- - [ ] Has `description` field (recommended)
48
+ - [ ] Has `description` field (required, localized object with all 4 locales)
30
49
  - [ ] Has `version` field (recommended)
31
50
  - [ ] Has `category` field (recommended)
32
51
  - [ ] Has `permissions` object (recommended, values are string arrays)
33
- - [ ] Has `commands` array (recommended, each with `id`, `title`, `route`)
52
+ - [ ] Has `commands` array (recommended, each with `id`, `title` (localized), `route`)
34
53
  - [ ] All command `route` values start with the module's `routeBase`
35
54
 
36
55
  Expected structure:
@@ -38,12 +57,14 @@ Expected structure:
38
57
  ```json
39
58
  {
40
59
  "id": "@admin/my-module",
41
- "title": "My Module",
60
+ "title": { "en-US": "My Module", "pt-BR": "Meu Módulo", "es": "Mi Módulo", "ro": "Modulul Meu" },
42
61
  "routeBase": "/my-module",
43
- "description": "My Module admin module",
62
+ "description": { "en-US": "My Module admin module", "pt-BR": "...", "es": "...", "ro": "..." },
44
63
  "version": "1.0.0",
45
64
  "category": "Tools",
46
- "commands": [{ "id": "home", "title": "Home", "route": "/my-module/home" }],
65
+ "commands": [
66
+ { "id": "home", "title": { "en-US": "Home", "pt-BR": "Início", "es": "Inicio", "ro": "Acasă" }, "route": "/my-module/home" }
67
+ ],
47
68
  "permissions": {
48
69
  "view": ["admin.my-module.view"],
49
70
  "edit": ["admin.my-module.edit"],
@@ -52,6 +73,14 @@ Expected structure:
52
73
  }
53
74
  ```
54
75
 
76
+ ## i18n
77
+
78
+ - [ ] All 4 locale files exist: `src/i18n/locales/en-US.json`, `pt-BR.json`, `es.json`, `ro.json`
79
+ - [ ] Manifest `title` and `description` are localized objects (not plain strings)
80
+ - [ ] Each command `title` is a localized object
81
+ - [ ] No manual i18n registration in `main.tsx` or `spa.tsx` (Vite plugin auto-injects)
82
+ - [ ] No `import "./i18n"` in `App.tsx`
83
+
55
84
  ## Dependencies (`package.json`)
56
85
 
57
86
  - [ ] `"type": "module"` is set
@@ -162,7 +191,7 @@ import { App } from "./App";
162
191
  export default App;
163
192
  ```
164
193
 
165
- ### `src/standalone.tsx`
194
+ ### `src/main.tsx`
166
195
 
167
196
  - [ ] Uses `AdminShell` from `@nsxbet/admin-sdk`
168
197
  - [ ] Imports manifest and passes to `modules` prop
@@ -220,7 +249,7 @@ If your module was scaffolded by Lovable, the following differences apply:
220
249
  - [ ] `vite.config.ts` uses `adminModule()` spread into the existing plugins array (option B above), NOT `defineModuleConfig`
221
250
  - [ ] React plugin is `@vitejs/plugin-react-swc` (Lovable default) — this is supported
222
251
  - [ ] `index.html` entry point references `/src/main.tsx` (Lovable default) — this serves as the standalone dev entry
223
- - [ ] `src/main.tsx` wraps your App with `AdminShell` (same role as `src/standalone.tsx` in non-Lovable modules)
252
+ - [ ] `src/main.tsx` wraps your App with `AdminShell` (same role as `src/main.tsx` in non-Lovable modules)
224
253
  - [ ] `src/spa.tsx` still required — `export default App` for shell mode
225
254
  - [ ] `admin.module.json` still required at project root
226
255
  - [ ] `module.manifest.json` is auto-generated at build time by `adminModule()` — do NOT manually copy `admin.module.json` to `dist/`
@@ -294,8 +323,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
294
323
 
295
324
  - [ ] No `@supabase/supabase-js` or `supabase` imports in `src/**`
296
325
  - [ ] No `firebase` imports in `src/**`
297
- - [ ] No `BrowserRouter` or `MemoryRouter` in `src/**` (except `standalone.tsx`)
326
+ - [ ] No `BrowserRouter` or `MemoryRouter` in `src/**` (except `main.tsx`)
298
327
  - [ ] No `useNavigate` imported from `@nsxbet/admin-sdk` (use `react-router-dom`)
299
328
  - [ ] No `@supabase/supabase-js` or `firebase` in `package.json` dependencies
300
329
  - [ ] No raw `fetch()` or `window.fetch()` calls — use `useFetch()` from the SDK
330
+ - [ ] No direct date formatting (`toLocaleDateString`, `toLocaleTimeString`, `Intl.DateTimeFormat`, dayjs/moment `.format()`, date-fns `format`) — use `<Timestamp />` or `useTimestamp()` from the SDK
301
331
  - [ ] Do NOT scaffold shadcn/ui components locally — use `@nsxbet/admin-ui` which ships them pre-configured
package/README.md CHANGED
@@ -41,7 +41,7 @@ npx @nsxbet/admin-cli check
41
41
  npx @nsxbet/admin-cli check --fix
42
42
  ```
43
43
 
44
- The CLI scaffolds all 13 required files with correct configuration, dependency versions, and SDK integration. It also validates existing modules against the canonical reference.
44
+ The CLI scaffolds all 17 required files (including 4 locale files) with correct configuration, dependency versions, and SDK integration. It also validates existing modules against the canonical reference.
45
45
 
46
46
  > **Required for LLMs:** After creating or modifying a module, run `npx @nsxbet/admin-cli checklist --output CHECKLIST.md` and verify every item in the generated file before considering the setup complete. The checklist is a self-contained verification guide—no CLI installation needed to consume it.
47
47
 
@@ -74,7 +74,7 @@ This SDK enables you to build admin modules that integrate with the NSX Admin sh
74
74
  | File | Purpose | When used |
75
75
  |------|---------|-----------|
76
76
  | `src/spa.tsx` | Default export of App | Shell loads this via React.lazy |
77
- | `src/standalone.tsx` | Full app with AdminShell wrapper | Local development (`npm run dev`) |
77
+ | `src/main.tsx` | Full app with AdminShell wrapper | Local development (`npm run dev`) |
78
78
 
79
79
  ## Complete Module Example
80
80
 
@@ -94,26 +94,53 @@ export default defineModuleConfig({
94
94
 
95
95
  ### File: `admin.module.json`
96
96
 
97
+ Title, description, and command titles must be **localized objects** with all 4 locales (`en-US`, `pt-BR`, `es`, `ro`):
98
+
97
99
  ```json
98
100
  {
99
101
  "id": "@admin/my-module",
100
- "title": "My Module",
101
- "description": "Description of what this module does",
102
- "version": "1.0.0",
102
+ "title": {
103
+ "en-US": "My Module",
104
+ "pt-BR": "Meu Módulo",
105
+ "es": "Mi Módulo",
106
+ "ro": "Modulul Meu"
107
+ },
108
+ "description": {
109
+ "en-US": "Description of what this module does",
110
+ "pt-BR": "Descrição do que este módulo faz",
111
+ "es": "Descripción de lo que hace este módulo",
112
+ "ro": "Descrierea a ceea ce face acest modul"
113
+ },
103
114
  "category": "Tools",
104
115
  "icon": "clipboard-list",
105
116
  "routeBase": "/my-module",
106
117
  "keywords": ["my", "module", "example"],
118
+ "navigation": {
119
+ "style": "stacked",
120
+ "sections": [
121
+ { "id": "general", "label": { "en-US": "General", "pt-BR": "Geral", "es": "General", "ro": "General" } }
122
+ ]
123
+ },
107
124
  "commands": [
108
125
  {
109
126
  "id": "list",
110
- "title": "List Items",
127
+ "title": {
128
+ "en-US": "List Items",
129
+ "pt-BR": "Listar Itens",
130
+ "es": "Listar Elementos",
131
+ "ro": "Lista Elemente"
132
+ },
111
133
  "route": "/my-module/list",
112
134
  "icon": "file-text"
113
135
  },
114
136
  {
115
137
  "id": "new",
116
- "title": "New Item",
138
+ "title": {
139
+ "en-US": "New Item",
140
+ "pt-BR": "Novo Item",
141
+ "es": "Nuevo Elemento",
142
+ "ro": "Element Nou"
143
+ },
117
144
  "route": "/my-module/new",
118
145
  "icon": "plus"
119
146
  }
@@ -142,7 +169,7 @@ import { App } from "./App";
142
169
  export default App;
143
170
  ```
144
171
 
145
- ### File: `src/standalone.tsx`
172
+ ### File: `src/main.tsx`
146
173
 
147
174
  ```tsx
148
175
  import React from "react";
@@ -150,7 +177,6 @@ import ReactDOM from "react-dom/client";
150
177
  import {
151
178
  AdminShell,
152
179
  initI18n,
153
- i18n,
154
180
  createInMemoryAuthClient,
155
181
  createMockUsersFromRoles,
156
182
  } from "@nsxbet/admin-sdk";
@@ -160,19 +186,10 @@ import manifest from "../admin.module.json";
160
186
 
161
187
  import "./index.css";
162
188
 
163
- // Import module translations (optional - for i18n support)
164
- import enUS from "./i18n/locales/en-US.json";
165
- import ptBR from "./i18n/locales/pt-BR.json";
166
-
167
- // Initialize i18n BEFORE shell renders
189
+ // Initialize i18n BEFORE shell renders.
190
+ // The Vite plugin (admin-module-i18n) auto-injects module translation registration into spa.tsx and main.tsx.
168
191
  initI18n();
169
192
 
170
- // Register module translations with namespace matching your module
171
- const NAMESPACE = "mymodule";
172
- i18n.addResourceBundle("en-US", NAMESPACE, enUS, true, true);
173
- i18n.addResourceBundle("pt-BR", NAMESPACE, ptBR, true, true);
174
-
175
- // Type assertion for JSON import
176
193
  const moduleManifest = manifest as AdminModuleManifest;
177
194
 
178
195
  // Check environment variable to toggle between mock auth and Keycloak
@@ -288,25 +305,27 @@ export function ItemList() {
288
305
  @tailwind utilities;
289
306
  ```
290
307
 
291
- ### Directory: `src/i18n/` (optional - for translations)
308
+ ### Directory: `src/i18n/` (required for translations)
309
+
310
+ All 4 locale files are **required** (`en-US`, `pt-BR`, `es`, `ro`). The Vite plugin validates this at build time.
292
311
 
293
312
  Structure:
294
313
 
295
314
  ```
296
315
  src/i18n/
297
316
  locales/
298
- en-US.json # English (required)
299
- pt-BR.json # Portuguese
317
+ en-US.json
318
+ pt-BR.json
319
+ es.json
320
+ ro.json
300
321
  ```
301
322
 
302
- Example `src/i18n/locales/en-US.json`:
323
+ **No manual registration needed.** The `admin-module-i18n` Vite plugin (included in `defineModuleConfig`) auto-injects `registerModuleTranslations` into `spa.tsx` and `main.tsx` at build/serve time. Just create the locale files.
324
+
325
+ Example `src/i18n/locales/en-US.json` (content translations only — nav titles live in `admin.module.json`):
303
326
 
304
327
  ```json
305
328
  {
306
- "module": {
307
- "title": "My Module",
308
- "description": "Description of my module"
309
- },
310
329
  "list": {
311
330
  "title": "All Items",
312
331
  "noItems": "No items found.",
@@ -327,13 +346,14 @@ Use translations in components:
327
346
  import { useI18n } from "@nsxbet/admin-sdk";
328
347
 
329
348
  function MyComponent() {
330
- const { t } = useI18n();
349
+ const { t } = useI18n("my-module");
331
350
 
332
- // Use with namespace prefix (set in standalone.tsx)
333
- return <h1>{t("mymodule:list.title")}</h1>;
351
+ return <h1>{t("list.title")}</h1>;
334
352
  }
335
353
  ```
336
354
 
355
+ The namespace is derived from your module id (e.g. `@admin/my-module` → `my-module`).
356
+
337
357
  ### File: `tailwind.config.js`
338
358
 
339
359
  Use `withAdminSdk` which automatically includes the UI preset and SDK/UI content paths:
@@ -423,7 +443,7 @@ export default withAdminSdk({
423
443
  </head>
424
444
  <body>
425
445
  <div id="root"></div>
426
- <script type="module" src="/src/standalone.tsx"></script>
446
+ <script type="module" src="/src/main.tsx"></script>
427
447
  </body>
428
448
  </html>
429
449
  ```
@@ -449,6 +469,7 @@ TypeScript declarations for environment variables and platform API:
449
469
  declare global {
450
470
  interface ImportMetaEnv {
451
471
  readonly VITE_MOCK_AUTH?: string;
472
+ readonly VITE_ALLOWED_MODULE_ORIGINS?: string;
452
473
  }
453
474
 
454
475
  interface ImportMeta {
@@ -497,6 +518,11 @@ VITE_MOCK_AUTH=true
497
518
 
498
519
  # Set to "false" to use Keycloak authentication
499
520
  # VITE_MOCK_AUTH=false
521
+
522
+ # Module URL allowlist (shell mode only). Comma-separated patterns.
523
+ # Supports *.domain wildcard (e.g. *.nsx.dev matches modules.nsx.dev, nsx.dev).
524
+ # In dev mode, localhost and 127.0.0.1 are always allowed.
525
+ # VITE_ALLOWED_MODULE_ORIGINS=*.nsx.dev,*.nsx.services
500
526
  ```
501
527
 
502
528
  ## Manifest Schema (`admin.module.json`)
@@ -507,10 +533,10 @@ VITE_MOCK_AUTH=true
507
533
  | `title` | string | ✅ | Human-readable title |
508
534
  | `routeBase` | string | ✅ | Base route path (must start with `/`) |
509
535
  | `description` | string | | What the module does |
510
- | `version` | string | | Semantic version |
511
536
  | `category` | string | | Navigation grouping |
512
537
  | `icon` | string | | Lucide icon name in kebab-case |
513
538
  | `keywords` | string[] | | Search keywords |
539
+ | `navigation` | object | | Navigation config (`style`, `sections`) |
514
540
  | `commands` | Command[] | | Available actions |
515
541
  | `permissions` | object | | Permission configuration |
516
542
  | `owners` | object | | Team ownership info |
@@ -524,6 +550,41 @@ VITE_MOCK_AUTH=true
524
550
  | `route` | string | ✅ | Full route path |
525
551
  | `icon` | string | | Lucide icon name |
526
552
  | `keywords` | string[] | | Search keywords |
553
+ | `section` | string | | Section ID for stacked navigation grouping |
554
+
555
+ ### Stacked Navigation
556
+
557
+ Modules with many commands can use stacked navigation for a dedicated sidebar panel with sectioned command grouping.
558
+
559
+ **Enable stacked navigation** by adding a `navigation` field to `admin.module.json`:
560
+
561
+ ```json
562
+ {
563
+ "navigation": {
564
+ "style": "stacked",
565
+ "sections": [
566
+ {
567
+ "id": "general",
568
+ "label": { "en-US": "General", "pt-BR": "Geral", "es": "General", "ro": "General" }
569
+ },
570
+ {
571
+ "id": "advanced",
572
+ "label": { "en-US": "Advanced", "pt-BR": "Avançado", "es": "Avanzado", "ro": "Avansat" }
573
+ }
574
+ ]
575
+ },
576
+ "commands": [
577
+ { "id": "list", "title": {...}, "route": "/mod/list", "section": "general" },
578
+ { "id": "settings", "title": {...}, "route": "/mod/settings", "section": "advanced" }
579
+ ]
580
+ }
581
+ ```
582
+
583
+ - `style`: `"stacked"` enables the dedicated panel; `"collapsible"` (default) keeps the inline expand behavior
584
+ - `sections`: Array of section definitions with `id` and localized `label`
585
+ - Commands reference sections via the `section` field matching a section `id`
586
+ - Commands without a `section` appear in an implicit top-level group
587
+ - The stacked panel is URL-driven: navigating to the module's `routeBase` activates it
527
588
 
528
589
  ### Icon Names
529
590
 
@@ -694,7 +755,10 @@ import adminPlugin from "@nsxbet/eslint-plugin-admin";
694
755
  export default [adminPlugin.configs.recommended];
695
756
  ```
696
757
 
697
- This enables all recommended rules including `@nsxbet/no-raw-fetch` which flags direct `fetch()`/`window.fetch()` calls that bypass authentication. Use `useFetch()` from the SDK instead.
758
+ This enables all recommended rules:
759
+
760
+ - **`@nsxbet/no-raw-fetch`** (error) — flags direct `fetch()`/`window.fetch()` calls that bypass authentication. Use `useFetch()` instead.
761
+ - **`@nsxbet/no-raw-date-format`** (warn) — flags direct date formatting (`toLocaleDateString`, `toLocaleTimeString`, `Intl.DateTimeFormat`, dayjs/moment `.format()`, date-fns `format`) that bypasses the platform timezone preference. Use `<Timestamp />` or `useTimestamp()` instead.
698
762
 
699
763
  ## SDK Hooks
700
764
 
@@ -719,7 +783,7 @@ function MyComponent() {
719
783
 
720
784
  | Method | Returns | Description |
721
785
  |--------|---------|-------------|
722
- | `hasPermission(perm)` | boolean | Check if user has permission |
786
+ | `hasPermission(perm)` | boolean | Check if user has permission. Returns `false` during Keycloak initialization until auth completes. |
723
787
  | `getUser()` | User | Get current user info |
724
788
  | `getAccessToken()` | Promise\<string\> | Get JWT token |
725
789
  | `logout()` | void | Log out user |
@@ -799,6 +863,132 @@ function MyComponent() {
799
863
  }
800
864
  ```
801
865
 
866
+ ### `useTimestamp()`
867
+
868
+ Timezone-aware date formatting that respects the shell's UTC/Local preference.
869
+
870
+ ```typescript
871
+ import { useTimestamp, Timestamp } from "@nsxbet/admin-sdk";
872
+
873
+ function MyComponent() {
874
+ const { mode, setMode, formatDate, timezone } = useTimestamp();
875
+
876
+ return (
877
+ <div>
878
+ <p>Timezone: {timezone} ({mode})</p>
879
+ <p>Formatted: {formatDate(new Date(), "datetime")}</p>
880
+ <Timestamp value={new Date()} format="date" />
881
+ </div>
882
+ );
883
+ }
884
+ ```
885
+
886
+ | Property | Type | Description |
887
+ |----------|------|-------------|
888
+ | `mode` | `"utc" \| "local"` | Current timezone mode |
889
+ | `setMode` | `(mode) => void` | Change the timezone mode |
890
+ | `formatDate` | `(date, format?) => string` | Format a date with current mode and locale |
891
+ | `timezone` | `string` | Resolved IANA timezone string (`"UTC"` or browser local) |
892
+
893
+ **Format presets:**
894
+
895
+ | Preset | Example (UTC, en-US) | Description |
896
+ |--------|----------------------|-------------|
897
+ | `"datetime"` (default) | "Mar 16, 2026, 2:30:05 PM UTC" | Full date and time |
898
+ | `"date"` | "Mar 16, 2026" | Date only |
899
+ | `"time"` | "2:30:05 PM UTC" | Time only |
900
+ | `"relative"` | "5 minutes ago" | Relative to now (timezone-independent) |
901
+
902
+ In shell mode, reads from `window.__ADMIN_PLATFORM_API__.timestamp`. In standalone mode, falls back to `localStorage` key `admin-timezone-mode` (default: `"local"`).
903
+
904
+ ### `<Timestamp />` Component
905
+
906
+ Renders a formatted date that automatically respects the shell's timezone preference.
907
+
908
+ ```tsx
909
+ import { Timestamp } from "@nsxbet/admin-sdk";
910
+
911
+ // Basic usage (datetime format)
912
+ <Timestamp value={new Date("2026-03-16T14:30:05Z")} />
913
+
914
+ // Date only
915
+ <Timestamp value={createdAt} format="date" />
916
+
917
+ // Relative time
918
+ <Timestamp value={updatedAt} format="relative" />
919
+
920
+ // String input (auto-parsed)
921
+ <Timestamp value="2026-03-16T14:30:05Z" format="time" />
922
+ ```
923
+
924
+ | Prop | Type | Default | Description |
925
+ |------|------|---------|-------------|
926
+ | `value` | `Date \| string` | required | The date to display |
927
+ | `format` | `TimestampFormat` | `"datetime"` | Format preset |
928
+ | `className` | `string` | | CSS class for the `<time>` element |
929
+
930
+ Renders a semantic `<time>` element with a `dateTime` attribute. Hovering shows a tooltip with the date in the opposite timezone mode.
931
+
932
+ **Migration guide:**
933
+
934
+ | Before | After |
935
+ |--------|-------|
936
+ | `date.toLocaleDateString()` | `<Timestamp value={date} format="date" />` |
937
+ | `date.toLocaleString()` | `<Timestamp value={date} />` |
938
+ | `date.toLocaleTimeString()` | `<Timestamp value={date} format="time" />` |
939
+ | `new Intl.DateTimeFormat(...).format(date)` | `<Timestamp value={date} />` |
940
+
941
+ ### `useRegistryPolling()`
942
+
943
+ Detect catalog changes via lightweight version polling. Used by the shell to show an "Updates available" banner without forcing a reload.
944
+
945
+ ```typescript
946
+ import { useRegistryPolling } from "@nsxbet/admin-sdk";
947
+
948
+ const { hasUpdates, dismiss } = useRegistryPolling({
949
+ registryClient,
950
+ initialVersion: catalog.version,
951
+ interval: 60000, // poll every 60s, 0 to disable
952
+ });
953
+ ```
954
+
955
+ | Property | Type | Description |
956
+ |----------|------|-------------|
957
+ | `hasUpdates` | boolean | `true` when the server version differs from the loaded version |
958
+ | `dismiss` | () => void | Hide the banner for the current version; re-shows on newer versions |
959
+
960
+ **Options:**
961
+
962
+ | Option | Type | Description |
963
+ |--------|------|-------------|
964
+ | `registryClient` | RegistryClient | The registry client instance |
965
+ | `initialVersion` | string | Version string from the initial `catalog.get()` response |
966
+ | `interval` | number \| undefined | Polling interval in ms. `0` or `undefined` disables polling |
967
+
968
+ The hook integrates the Page Visibility API — polling pauses when the tab is hidden and resumes with an immediate check when the tab becomes visible again. Network errors are silently ignored.
969
+
970
+ #### `catalog.version()`
971
+
972
+ Both HTTP and in-memory registry clients expose a `catalog.version()` method:
973
+
974
+ ```typescript
975
+ const { version, generatedAt } = await registryClient.catalog.version();
976
+ ```
977
+
978
+ The HTTP client calls `GET /api/catalog/version`. The in-memory client derives a version from the local mutation state.
979
+
980
+ #### Environment configuration
981
+
982
+ The polling interval is resolved from `REGISTRY_POLL_INTERVAL` (milliseconds):
983
+
984
+ | Source | Example |
985
+ |--------|---------|
986
+ | `window.__ENV__.REGISTRY_POLL_INTERVAL` | Docker runtime injection |
987
+ | `import.meta.env.VITE_REGISTRY_POLL_INTERVAL` | Vite env var |
988
+ | Environment default | `local`: 60s, `staging`: 5min, `production`: 15min |
989
+
990
+ Set `REGISTRY_POLL_INTERVAL=0` to disable polling entirely.
991
+
802
992
  ### Navigation
803
993
 
804
994
  **Use `useNavigate` from `react-router-dom` directly:**
@@ -847,7 +1037,7 @@ const mockUsers = createMockUsersFromRoles({
847
1037
  // Create auth client with custom users
848
1038
  const authClient = createInMemoryAuthClient({ users: mockUsers });
849
1039
 
850
- // Use in standalone.tsx
1040
+ // Use in main.tsx
851
1041
  ReactDOM.createRoot(document.getElementById("root")!).render(
852
1042
  <AdminShell authClient={authClient} modules={[manifest]}>
853
1043
  <App />
@@ -892,6 +1082,48 @@ const authClient = createInMemoryAuthClient({ users: customUsers });
892
1082
  |--------|------|---------|-------------|
893
1083
  | `users` | MockUser[] | **required** | Mock users available for selection |
894
1084
  | `storageKey` | string | `"@nsxbet/auth"` | localStorage key for persistence |
1085
+ | `gatewayUrl` | string \| null | `VITE_ADMIN_GATEWAY_URL` env var | Admin gateway URL for real JWT tokens |
1086
+ | `tokenTimeout` | number | `5000` | Timeout in ms for gateway token fetch |
1087
+
1088
+ ### Gateway Token Integration (BFF)
1089
+
1090
+ When `VITE_ADMIN_GATEWAY_URL` is set (or `gatewayUrl` is passed explicitly), the InMemory auth client fetches real signed JWTs from the admin gateway instead of returning mock token strings. This enables end-to-end testing against backends that validate tokens.
1091
+
1092
+ **How it works:**
1093
+
1094
+ 1. Developer clicks a user card in the UserSelector
1095
+ 2. The client calls `GET {gatewayUrl}/auth/token?sub=...&email=...&roles=...&scopes=...`
1096
+ 3. The gateway returns a signed JWT which is cached in memory
1097
+ 4. `getAccessToken()` returns the cached JWT for all subsequent API calls
1098
+ 5. When the token nears expiry (within 60s), a background refresh is triggered
1099
+
1100
+ **Gateway URL resolution order:**
1101
+
1102
+ 1. Explicit `gatewayUrl` option passed to `createInMemoryAuthClient()`
1103
+ 2. `import.meta.env.VITE_ADMIN_GATEWAY_URL` environment variable
1104
+ 3. If neither is set, BFF integration is disabled (mock tokens, current behavior)
1105
+
1106
+ **Error handling:**
1107
+
1108
+ The UserSelector shows loading, error, and timeout states during the token fetch. If the gateway is unreachable, the developer can:
1109
+ - **Retry** the token fetch
1110
+ - **Continue with mock token** to fall back to legacy behavior
1111
+ - **Go back** to the user selection list
1112
+
1113
+ ```bash
1114
+ # Add to your .env file to enable gateway token integration
1115
+ VITE_ADMIN_GATEWAY_URL=https://admin-bff-stg.nsx.dev
1116
+ ```
1117
+
1118
+ ## Module URL Allowlist (Shell Mode)
1119
+
1120
+ When the shell loads modules dynamically from URLs, it validates each URL against `VITE_ALLOWED_MODULE_ORIGINS` before `import()` or `loadScript()`.
1121
+
1122
+ **Format:** Comma-separated patterns supporting `*.domain` wildcard (e.g. `*.nsx.dev` matches `modules.nsx.dev`, `cdn.nsx.dev`, and apex `nsx.dev`).
1123
+
1124
+ **Dev mode:** `localhost` and `127.0.0.1` are always allowed regardless of the allowlist, so local dev servers work without configuration.
1125
+
1126
+ **Production:** Set `VITE_ALLOWED_MODULE_ORIGINS` in your build environment. If unset or empty, all module loads fail with a clear error.
895
1127
 
896
1128
  ## Keycloak Configuration (Production Auth)
897
1129
 
@@ -929,6 +1161,10 @@ Or pass the `keycloak` prop directly without `authClient`:
929
1161
  </AdminShell>
930
1162
  ```
931
1163
 
1164
+ ### Mock Auth Production Guard
1165
+
1166
+ In production builds, AdminShell requires authentication configuration. If you omit both `authClient` and `keycloak` props, the shell throws an error unless you explicitly set `VITE_MOCK_AUTH=true`. When mock auth is used in production with `VITE_MOCK_AUTH=true`, a console warning is logged. Always use Keycloak (or another real auth client) for production deployments.
1167
+
932
1168
  ### Keycloak Configuration Options
933
1169
 
934
1170
  | Option | Type | Description |
@@ -1026,6 +1262,29 @@ const fetch = useFetch();
1026
1262
  const data = await fetch("/api/internal-endpoint");
1027
1263
  ```
1028
1264
 
1265
+ ### ❌ DO NOT format dates directly
1266
+
1267
+ ```typescript
1268
+ // WRONG - bypasses platform timezone preference
1269
+ date.toLocaleDateString(); // ❌
1270
+ date.toLocaleTimeString(); // ❌
1271
+ new Intl.DateTimeFormat("en-US").format(date); // ❌
1272
+ dayjs(date).format("YYYY-MM-DD"); // ❌
1273
+ import { format } from "date-fns"; format(date, "PP"); // ❌
1274
+ ```
1275
+
1276
+ ```tsx
1277
+ // CORRECT - use <Timestamp /> or useTimestamp()
1278
+ import { Timestamp, useTimestamp } from "@nsxbet/admin-sdk";
1279
+
1280
+ <Timestamp value={date} format="date" /> // ✅
1281
+
1282
+ const { formatDate } = useTimestamp();
1283
+ formatDate(date, "datetime"); // ✅
1284
+ ```
1285
+
1286
+ Direct formatting bypasses the platform timezone preference, causing inconsistent timestamp display across modules. The `@nsxbet/no-raw-date-format` ESLint rule enforces this.
1287
+
1029
1288
  ### ❌ DO NOT import useNavigate from SDK
1030
1289
 
1031
1290
  ```typescript
@@ -1091,6 +1350,43 @@ bun run build
1091
1350
  bun run preview
1092
1351
  ```
1093
1352
 
1353
+ ## Error Boundary with Module Ownership
1354
+
1355
+ When a module crashes at runtime, the error boundary displays ownership information to help with incident triage. The `DynamicModule` component accepts an optional `moduleInfo` prop that provides module metadata to the error boundary.
1356
+
1357
+ ```tsx
1358
+ <DynamicModule
1359
+ baseUrl="http://localhost:5001"
1360
+ moduleInfo={{
1361
+ id: "@admin/payments",
1362
+ title: { "en-US": "Payments", "pt-BR": "Pagamentos", "es": "Pagos", "ro": "Plăți" },
1363
+ owners: { team: "Payments", supportChannel: "#payments-support" },
1364
+ }}
1365
+ />
1366
+ ```
1367
+
1368
+ When a module error occurs, the error boundary will:
1369
+ - Display the module name (localized for the current locale)
1370
+ - Show the owner team and support channel (if provided)
1371
+ - Render the support channel as a clickable link if it starts with `http`/`https`
1372
+ - Provide both "Try Again" (re-render) and "Reload Page" (full reload) buttons
1373
+ - Report the error to telemetry with module attribution (`moduleId`, `ownerTeam`, `errorType`)
1374
+ - Capture unhandled promise rejections while the module is mounted and report them via telemetry
1375
+
1376
+ If `owners.team` is empty, the ownership section is omitted and the error boundary shows a standard error UI.
1377
+
1378
+ ## Platform API — Timestamp Namespace
1379
+
1380
+ The shell exposes `window.__ADMIN_PLATFORM_API__.timestamp` for timezone preference management:
1381
+
1382
+ | Property | Type | Description |
1383
+ |----------|------|-------------|
1384
+ | `mode` | `"utc" \| "local"` | Current timezone mode |
1385
+ | `setMode(mode)` | `(TimezoneMode) => void` | Change the timezone mode |
1386
+ | `onModeChange(cb)` | `(callback) => () => void` | Subscribe to changes; returns unsubscribe |
1387
+
1388
+ > **Note:** Modules should use `useTimestamp()` or `<Timestamp />` rather than accessing the Platform API directly.
1389
+
1094
1390
  ## Types
1095
1391
 
1096
1392
  ```typescript
@@ -1100,6 +1396,11 @@ import type {
1100
1396
  PlatformAPI,
1101
1397
  User,
1102
1398
  Breadcrumb,
1399
+ ModuleInfo,
1400
+ ErrorBoundaryProps,
1401
+ TimezoneMode,
1402
+ TimestampFormat,
1403
+ UseTimestampResult,
1103
1404
  } from "@nsxbet/admin-sdk";
1104
1405
  ```
1105
1406
 
@@ -0,0 +1,19 @@
1
+ import type { MockUser } from './interface';
2
+ export declare class GatewayTimeoutError extends Error {
3
+ readonly gatewayUrl: string;
4
+ readonly timeoutMs: number;
5
+ constructor(gatewayUrl: string, timeoutMs: number);
6
+ }
7
+ export declare class GatewayFetchError extends Error {
8
+ readonly gatewayUrl: string;
9
+ readonly statusCode?: number | undefined;
10
+ readonly originalError?: unknown | undefined;
11
+ constructor(gatewayUrl: string, statusCode?: number | undefined, originalError?: unknown | undefined);
12
+ }
13
+ export interface GatewayTokenResult {
14
+ token: string;
15
+ expiresAt: number;
16
+ }
17
+ export declare function fetchGatewayToken(gatewayUrl: string, user: MockUser, options?: {
18
+ timeoutMs?: number;
19
+ }): Promise<GatewayTokenResult>;