@hed-hog/core 0.0.370 → 0.0.373

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 (248) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +2 -0
  3. package/dist/ai/ai.service.d.ts.map +1 -1
  4. package/dist/ai/ai.service.js +10 -7
  5. package/dist/ai/ai.service.js.map +1 -1
  6. package/dist/app-command/app-command.module.d.ts +3 -0
  7. package/dist/app-command/app-command.module.d.ts.map +1 -0
  8. package/dist/app-command/app-command.module.js +22 -0
  9. package/dist/app-command/app-command.module.js.map +1 -0
  10. package/dist/app-command/app-command.registry.d.ts +22 -0
  11. package/dist/app-command/app-command.registry.d.ts.map +1 -0
  12. package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
  13. package/dist/app-command/app-command.registry.js.map +1 -0
  14. package/dist/auth/auth.controller.d.ts +2 -2
  15. package/dist/auth/auth.controller.d.ts.map +1 -1
  16. package/dist/auth/auth.controller.js +20 -7
  17. package/dist/auth/auth.controller.js.map +1 -1
  18. package/dist/auth/auth.service.d.ts +3 -3
  19. package/dist/auth/auth.service.d.ts.map +1 -1
  20. package/dist/auth/auth.service.js +10 -36
  21. package/dist/auth/auth.service.js.map +1 -1
  22. package/dist/auth/dto/forget.dto.d.ts +1 -0
  23. package/dist/auth/dto/forget.dto.d.ts.map +1 -1
  24. package/dist/auth/dto/forget.dto.js +5 -0
  25. package/dist/auth/dto/forget.dto.js.map +1 -1
  26. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  27. package/dist/auth/guards/auth.guard.js +4 -1
  28. package/dist/auth/guards/auth.guard.js.map +1 -1
  29. package/dist/challenge/challenge.service.d.ts +8 -7
  30. package/dist/challenge/challenge.service.d.ts.map +1 -1
  31. package/dist/challenge/challenge.service.js +27 -19
  32. package/dist/challenge/challenge.service.js.map +1 -1
  33. package/dist/file/dto/upload.dto.d.ts +3 -0
  34. package/dist/file/dto/upload.dto.d.ts.map +1 -1
  35. package/dist/file/dto/upload.dto.js +22 -0
  36. package/dist/file/dto/upload.dto.js.map +1 -1
  37. package/dist/file/file.controller.d.ts +2 -1
  38. package/dist/file/file.controller.d.ts.map +1 -1
  39. package/dist/file/file.controller.js +29 -16
  40. package/dist/file/file.controller.js.map +1 -1
  41. package/dist/file/file.service.d.ts +51 -1
  42. package/dist/file/file.service.d.ts.map +1 -1
  43. package/dist/file/file.service.js +250 -18
  44. package/dist/file/file.service.js.map +1 -1
  45. package/dist/file/image-optimization.service.d.ts +17 -4
  46. package/dist/file/image-optimization.service.d.ts.map +1 -1
  47. package/dist/file/image-optimization.service.js +101 -46
  48. package/dist/file/image-optimization.service.js.map +1 -1
  49. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  50. package/dist/file/provider/s3.provider.js +15 -7
  51. package/dist/file/provider/s3.provider.js.map +1 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +18 -8
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +4 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
  60. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
  61. package/dist/integration/services/event-subscriber.registry.js +15 -0
  62. package/dist/integration/services/event-subscriber.registry.js.map +1 -1
  63. package/dist/integration/services/outbox.processor.d.ts.map +1 -1
  64. package/dist/integration/services/outbox.processor.js +2 -4
  65. package/dist/integration/services/outbox.processor.js.map +1 -1
  66. package/dist/integration/services/outbox.service.d.ts +4 -2
  67. package/dist/integration/services/outbox.service.d.ts.map +1 -1
  68. package/dist/integration/services/outbox.service.js +26 -15
  69. package/dist/integration/services/outbox.service.js.map +1 -1
  70. package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
  71. package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
  72. package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
  73. package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
  74. package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
  75. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  76. package/dist/integration-profile/integration-profile.controller.js +4 -3
  77. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  78. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
  79. package/dist/integration-profile/integration-profile.module.js +5 -2
  80. package/dist/integration-profile/integration-profile.module.js.map +1 -1
  81. package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
  82. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
  83. package/dist/integration-profile/integration-profile.secrets.js +56 -0
  84. package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
  85. package/dist/integration-profile/integration-profile.service.d.ts +125 -8
  86. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  87. package/dist/integration-profile/integration-profile.service.js +257 -68
  88. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  89. package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
  90. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  91. package/dist/integration-profile/integration-profile.utils.js +31 -0
  92. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  93. package/dist/language/en.json +178 -0
  94. package/dist/language/pt.json +178 -0
  95. package/dist/mail/mail.controller.d.ts.map +1 -1
  96. package/dist/mail/mail.controller.js +5 -28
  97. package/dist/mail/mail.controller.js.map +1 -1
  98. package/dist/mail/mail.module.js +18 -18
  99. package/dist/mail/mail.module.js.map +1 -1
  100. package/dist/mail/mail.service.d.ts +13 -2
  101. package/dist/mail/mail.service.d.ts.map +1 -1
  102. package/dist/mail/mail.service.js +25 -15
  103. package/dist/mail/mail.service.js.map +1 -1
  104. package/dist/mcp-chat/mcp-chat.service.js +6 -5
  105. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  106. package/dist/oauth/providers/facebook.provider.js +5 -5
  107. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  108. package/dist/oauth/providers/github.provider.js +5 -5
  109. package/dist/oauth/providers/github.provider.js.map +1 -1
  110. package/dist/oauth/providers/google.provider.js +5 -5
  111. package/dist/oauth/providers/google.provider.js.map +1 -1
  112. package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
  113. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  114. package/dist/oauth/providers/microsoft.provider.js +5 -5
  115. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  116. package/dist/pdf/pdf.service.d.ts +4 -0
  117. package/dist/pdf/pdf.service.d.ts.map +1 -1
  118. package/dist/pdf/pdf.service.js +16 -9
  119. package/dist/pdf/pdf.service.js.map +1 -1
  120. package/dist/profile/profile.controller.d.ts +7 -7
  121. package/dist/profile/profile.controller.d.ts.map +1 -1
  122. package/dist/profile/profile.controller.js +28 -21
  123. package/dist/profile/profile.controller.js.map +1 -1
  124. package/dist/profile/profile.service.d.ts +7 -7
  125. package/dist/profile/profile.service.d.ts.map +1 -1
  126. package/dist/profile/profile.service.js +21 -25
  127. package/dist/profile/profile.service.js.map +1 -1
  128. package/dist/setting/setting.service.d.ts +1 -0
  129. package/dist/setting/setting.service.d.ts.map +1 -1
  130. package/dist/setting/setting.service.js +15 -2
  131. package/dist/setting/setting.service.js.map +1 -1
  132. package/dist/token/token.service.d.ts.map +1 -1
  133. package/dist/token/token.service.js +2 -1
  134. package/dist/token/token.service.js.map +1 -1
  135. package/dist/user/user.controller.d.ts +9 -0
  136. package/dist/user/user.controller.d.ts.map +1 -1
  137. package/dist/user/user.controller.js +13 -0
  138. package/dist/user/user.controller.js.map +1 -1
  139. package/dist/user/user.service.d.ts +7 -0
  140. package/dist/user/user.service.d.ts.map +1 -1
  141. package/dist/user/user.service.js +19 -0
  142. package/dist/user/user.service.js.map +1 -1
  143. package/dist/utils/webauthn-origin.d.ts +11 -0
  144. package/dist/utils/webauthn-origin.d.ts.map +1 -0
  145. package/dist/utils/webauthn-origin.js +37 -0
  146. package/dist/utils/webauthn-origin.js.map +1 -0
  147. package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
  148. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  149. package/dist/webhook-integration/webhook-integration.controller.js +29 -0
  150. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  151. package/dist/webhook-integration/webhook-integration.module.js +3 -3
  152. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  153. package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
  154. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  155. package/dist/webhook-integration/webhook-integration.service.js +110 -6
  156. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  157. package/hedhog/data/integration_provider.yaml +9 -0
  158. package/hedhog/data/integration_type.yaml +6 -0
  159. package/hedhog/data/menu.yaml +6 -0
  160. package/hedhog/data/role.yaml +7 -0
  161. package/hedhog/data/route.yaml +36 -0
  162. package/hedhog/data/setting_group.yaml +119 -13
  163. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  164. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  165. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  166. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  168. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  169. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  171. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  172. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  173. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  174. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  176. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  177. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +156 -38
  178. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  179. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  180. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  181. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  182. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  183. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  184. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  185. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  187. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  189. package/hedhog/frontend/messages/en.json +69 -4
  190. package/hedhog/frontend/messages/pt.json +69 -4
  191. package/hedhog/table/setting.yaml +2 -0
  192. package/hedhog/table/webhook_call_log.yaml +49 -46
  193. package/package.json +9 -4
  194. package/src/ai/ai.service.ts +9 -6
  195. package/src/app-command/app-command.module.ts +9 -0
  196. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  197. package/src/auth/auth.controller.ts +20 -4
  198. package/src/auth/auth.service.ts +10 -36
  199. package/src/auth/dto/forget.dto.ts +5 -1
  200. package/src/auth/guards/auth.guard.ts +4 -1
  201. package/src/challenge/challenge.service.ts +30 -19
  202. package/src/file/dto/upload.dto.ts +25 -1
  203. package/src/file/file.controller.ts +31 -18
  204. package/src/file/file.service.ts +341 -16
  205. package/src/file/image-optimization.service.ts +111 -46
  206. package/src/file/provider/s3.provider.ts +16 -7
  207. package/src/file/provider/spaces.provider.ts +15 -5
  208. package/src/index.ts +4 -1
  209. package/src/integration/services/event-subscriber.registry.ts +15 -0
  210. package/src/integration/services/outbox.processor.ts +2 -5
  211. package/src/integration/services/outbox.service.ts +35 -17
  212. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  213. package/src/integration-profile/integration-profile.controller.ts +2 -0
  214. package/src/integration-profile/integration-profile.module.ts +5 -2
  215. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  216. package/src/integration-profile/integration-profile.service.ts +335 -54
  217. package/src/integration-profile/integration-profile.utils.ts +56 -0
  218. package/src/language/en.json +5 -1
  219. package/src/language/pt.json +5 -1
  220. package/src/mail/mail.controller.ts +5 -29
  221. package/src/mail/mail.module.ts +5 -5
  222. package/src/mail/mail.service.ts +37 -13
  223. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  224. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  225. package/src/oauth/providers/facebook.provider.ts +3 -3
  226. package/src/oauth/providers/github.provider.ts +3 -3
  227. package/src/oauth/providers/google.provider.ts +3 -3
  228. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  229. package/src/oauth/providers/microsoft.provider.ts +3 -3
  230. package/src/pdf/pdf.service.ts +19 -6
  231. package/src/profile/profile.controller.ts +21 -14
  232. package/src/profile/profile.service.ts +23 -20
  233. package/src/security/security.service.spec.ts +222 -0
  234. package/src/setting/setting.service.ts +15 -2
  235. package/src/token/token.service.ts +2 -1
  236. package/src/user/user.controller.ts +26 -16
  237. package/src/user/user.service.ts +25 -0
  238. package/src/utils/webauthn-origin.ts +38 -0
  239. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  240. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  241. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  242. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  243. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  244. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  245. package/src/menu/menu.service.spec.ts +0 -247
  246. package/src/route/route.service.spec.ts +0 -300
  247. package/src/screen/screen.service.spec.ts +0 -298
  248. package/src/user/user.service.spec.ts +0 -294
@@ -68,7 +68,7 @@ import {
68
68
  IconX,
69
69
  } from '@tabler/icons-react';
70
70
  import { useTranslations } from 'next-intl';
71
- import { useState } from 'react';
71
+ import { useMemo, useState } from 'react';
72
72
  import { toast } from 'sonner';
73
73
 
74
74
  interface Component {
@@ -111,6 +111,8 @@ interface RoleOption {
111
111
  name?: string;
112
112
  }
113
113
 
114
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
115
+
114
116
  export function ComponentsTab() {
115
117
  const t = useTranslations('core.DashboardManagement');
116
118
  const { request, locales, currentLocaleCode } = useApp();
@@ -133,10 +135,40 @@ export function ComponentsTab() {
133
135
  useState<Component | null>(null);
134
136
  const [selectedRoleIds, setSelectedRoleIds] = useState<number[]>([]);
135
137
  const [page, setPage] = useState(1);
138
+ const { data: generalSettings } = useQuery<{
139
+ data: Array<{ slug: string; value: string }>;
140
+ }>({
141
+ queryKey: ['setting-group-general'],
142
+ queryFn: async () => {
143
+ const response = await request<{
144
+ data: Array<{ slug: string; value: string }>;
145
+ }>({ url: '/setting/group/general', method: 'GET' });
146
+ return response.data;
147
+ },
148
+ staleTime: 5 * 60 * 1000,
149
+ });
150
+
151
+ const pageSizeOptions = useMemo(() => {
152
+ const setting = generalSettings?.data?.find(
153
+ (s) => s.slug === 'pagination-page-sizes'
154
+ );
155
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
156
+ try {
157
+ const parsed = JSON.parse(setting.value) as string[];
158
+ const sizes = parsed
159
+ .map(Number)
160
+ .filter((n) => !isNaN(n) && n > 0)
161
+ .sort((a, b) => a - b);
162
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
163
+ } catch {
164
+ return DEFAULT_PAGE_SIZES;
165
+ }
166
+ }, [generalSettings]);
167
+
136
168
  const [pageSize, setPageSize] = usePersistedPageSize({
137
169
  storageKey: 'pagination:global:pageSize',
138
- defaultValue: 10,
139
- allowedValues: [10, 20, 30, 50],
170
+ defaultValue: 12,
171
+ allowedValues: pageSizeOptions,
140
172
  });
141
173
  const [searchQuery, setSearchQuery] = useState('');
142
174
  const [roleSearchQuery, setRoleSearchQuery] = useState('');
@@ -909,7 +941,7 @@ export function ComponentsTab() {
909
941
  <SelectValue />
910
942
  </SelectTrigger>
911
943
  <SelectContent>
912
- {[10, 20, 30, 50].map((size) => (
944
+ {pageSizeOptions.map((size) => (
913
945
  <SelectItem key={size} value={String(size)}>
914
946
  {size}
915
947
  </SelectItem>
@@ -133,6 +133,8 @@ type RequestError = {
133
133
  };
134
134
  };
135
135
 
136
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
137
+
136
138
  export function DashboardRolesTab() {
137
139
  const t = useTranslations('core.DashboardManagement');
138
140
  const { request, currentLocaleCode, getSettingValue } = useApp();
@@ -146,10 +148,40 @@ export function DashboardRolesTab() {
146
148
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
147
149
  const [roleToDelete, setRoleToDelete] = useState<number | null>(null);
148
150
  const [page, setPage] = useState(1);
151
+ const { data: generalSettings } = useQuery<{
152
+ data: Array<{ slug: string; value: string }>;
153
+ }>({
154
+ queryKey: ['setting-group-general'],
155
+ queryFn: async () => {
156
+ const response = await request<{
157
+ data: Array<{ slug: string; value: string }>;
158
+ }>({ url: '/setting/group/general', method: 'GET' });
159
+ return response.data;
160
+ },
161
+ staleTime: 5 * 60 * 1000,
162
+ });
163
+
164
+ const pageSizeOptions = useMemo(() => {
165
+ const setting = generalSettings?.data?.find(
166
+ (s) => s.slug === 'pagination-page-sizes'
167
+ );
168
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
169
+ try {
170
+ const parsed = JSON.parse(setting.value) as string[];
171
+ const sizes = parsed
172
+ .map(Number)
173
+ .filter((n) => !isNaN(n) && n > 0)
174
+ .sort((a, b) => a - b);
175
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
176
+ } catch {
177
+ return DEFAULT_PAGE_SIZES;
178
+ }
179
+ }, [generalSettings]);
180
+
149
181
  const [pageSize, setPageSize] = usePersistedPageSize({
150
182
  storageKey: 'pagination:global:pageSize',
151
- defaultValue: 10,
152
- allowedValues: [10, 20, 30, 50],
183
+ defaultValue: 12,
184
+ allowedValues: pageSizeOptions,
153
185
  });
154
186
  const searchQuery = '';
155
187
  const debouncedSearch = useDebounce(searchQuery, 500);
@@ -539,7 +571,7 @@ export function DashboardRolesTab() {
539
571
  <SelectValue />
540
572
  </SelectTrigger>
541
573
  <SelectContent>
542
- {[10, 20, 30, 50].map((size) => (
574
+ {pageSizeOptions.map((size) => (
543
575
  <SelectItem key={size} value={String(size)}>
544
576
  {size}
545
577
  </SelectItem>
@@ -67,7 +67,7 @@ import {
67
67
  IconX,
68
68
  } from '@tabler/icons-react';
69
69
  import { useTranslations } from 'next-intl';
70
- import { useState } from 'react';
70
+ import { useMemo, useState } from 'react';
71
71
  import { toast } from 'sonner';
72
72
 
73
73
  interface Dashboard {
@@ -98,6 +98,8 @@ interface RoleOption {
98
98
  name?: string;
99
99
  }
100
100
 
101
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
102
+
101
103
  export function DashboardsTab() {
102
104
  const t = useTranslations('core.DashboardManagement');
103
105
  const { request, locales, currentLocaleCode } = useApp();
@@ -120,10 +122,40 @@ export function DashboardsTab() {
120
122
  useState<Dashboard | null>(null);
121
123
  const [selectedRoleIds, setSelectedRoleIds] = useState<number[]>([]);
122
124
  const [page, setPage] = useState(1);
125
+ const { data: generalSettings } = useQuery<{
126
+ data: Array<{ slug: string; value: string }>;
127
+ }>({
128
+ queryKey: ['setting-group-general'],
129
+ queryFn: async () => {
130
+ const response = await request<{
131
+ data: Array<{ slug: string; value: string }>;
132
+ }>({ url: '/setting/group/general', method: 'GET' });
133
+ return response.data;
134
+ },
135
+ staleTime: 5 * 60 * 1000,
136
+ });
137
+
138
+ const pageSizeOptions = useMemo(() => {
139
+ const setting = generalSettings?.data?.find(
140
+ (s) => s.slug === 'pagination-page-sizes'
141
+ );
142
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
143
+ try {
144
+ const parsed = JSON.parse(setting.value) as string[];
145
+ const sizes = parsed
146
+ .map(Number)
147
+ .filter((n) => !isNaN(n) && n > 0)
148
+ .sort((a, b) => a - b);
149
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
150
+ } catch {
151
+ return DEFAULT_PAGE_SIZES;
152
+ }
153
+ }, [generalSettings]);
154
+
123
155
  const [pageSize, setPageSize] = usePersistedPageSize({
124
156
  storageKey: 'pagination:global:pageSize',
125
- defaultValue: 10,
126
- allowedValues: [10, 20, 30, 50],
157
+ defaultValue: 12,
158
+ allowedValues: pageSizeOptions,
127
159
  });
128
160
  const [searchQuery, setSearchQuery] = useState('');
129
161
  const [roleSearchQuery, setRoleSearchQuery] = useState('');
@@ -653,7 +685,7 @@ export function DashboardsTab() {
653
685
  <SelectValue />
654
686
  </SelectTrigger>
655
687
  <SelectContent>
656
- {[10, 20, 30, 50].map((size) => (
688
+ {pageSizeOptions.map((size) => (
657
689
  <SelectItem key={size} value={String(size)}>
658
690
  {size}
659
691
  </SelectItem>
@@ -127,6 +127,8 @@ type ComponentListResponse = {
127
127
  data?: Component[];
128
128
  };
129
129
 
130
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
131
+
130
132
  export function ItemsTab() {
131
133
  const t = useTranslations('core.DashboardManagement');
132
134
  const { request, currentLocaleCode, getSettingValue } = useApp();
@@ -144,10 +146,40 @@ export function ItemsTab() {
144
146
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
145
147
  const [itemToDelete, setItemToDelete] = useState<number | null>(null);
146
148
  const [page, setPage] = useState(1);
149
+ const { data: generalSettings } = useQuery<{
150
+ data: Array<{ slug: string; value: string }>;
151
+ }>({
152
+ queryKey: ['setting-group-general'],
153
+ queryFn: async () => {
154
+ const response = await request<{
155
+ data: Array<{ slug: string; value: string }>;
156
+ }>({ url: '/setting/group/general', method: 'GET' });
157
+ return response.data;
158
+ },
159
+ staleTime: 5 * 60 * 1000,
160
+ });
161
+
162
+ const pageSizeOptions = useMemo(() => {
163
+ const setting = generalSettings?.data?.find(
164
+ (s) => s.slug === 'pagination-page-sizes'
165
+ );
166
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
167
+ try {
168
+ const parsed = JSON.parse(setting.value) as string[];
169
+ const sizes = parsed
170
+ .map(Number)
171
+ .filter((n) => !isNaN(n) && n > 0)
172
+ .sort((a, b) => a - b);
173
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
174
+ } catch {
175
+ return DEFAULT_PAGE_SIZES;
176
+ }
177
+ }, [generalSettings]);
178
+
147
179
  const [pageSize, setPageSize] = usePersistedPageSize({
148
180
  storageKey: 'pagination:global:pageSize',
149
- defaultValue: 10,
150
- allowedValues: [10, 20, 30, 50],
181
+ defaultValue: 12,
182
+ allowedValues: pageSizeOptions,
151
183
  });
152
184
  const searchQuery = '';
153
185
  const debouncedSearch = useDebounce(searchQuery, 500);
@@ -647,7 +679,7 @@ export function ItemsTab() {
647
679
  <SelectValue />
648
680
  </SelectTrigger>
649
681
  <SelectContent>
650
- {[10, 20, 30, 50].map((size) => (
682
+ {pageSizeOptions.map((size) => (
651
683
  <SelectItem key={size} value={String(size)}>
652
684
  {size}
653
685
  </SelectItem>
@@ -19,6 +19,7 @@ import {
19
19
  AlertDialogHeader,
20
20
  AlertDialogTitle,
21
21
  } from '@/components/ui/alert-dialog';
22
+ import { DoNodePoolPicker } from '@/components/integration-profile/do-node-pool-picker';
22
23
  import { Avatar, AvatarFallback } from '@/components/ui/avatar';
23
24
  import { Badge } from '@/components/ui/badge';
24
25
  import { Button } from '@/components/ui/button';
@@ -57,6 +58,7 @@ import {
57
58
  TableHeader,
58
59
  TableRow,
59
60
  } from '@/components/ui/table';
61
+ import { TableBodySkeleton } from '@/components/ui/skeletons';
60
62
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
61
63
  import { usePagination } from '@/hooks/use-pagination';
62
64
  import { formatDate } from '@/lib/format-date';
@@ -166,6 +168,9 @@ type ImportValidationResponse = {
166
168
 
167
169
  // ─── Provider field definitions ───────────────────────────────────────────────
168
170
 
171
+ /** Placeholder devolvido pelo backend no lugar de um segredo armazenado. */
172
+ const SECRET_MASK = '********';
173
+
169
174
  const PROVIDER_FIELDS: Record<string, FieldDef[]> = {
170
175
  smtp: [
171
176
  {
@@ -653,6 +658,61 @@ const PROVIDER_FIELDS: Record<string, FieldDef[]> = {
653
658
  placeholder: 'https://eu.altcha.org/api/v1',
654
659
  },
655
660
  ],
661
+ // O node pool de vídeo NÃO é um campo de texto: é escolhido por nome via picker
662
+ // dedicado (carrega os pools da DO API com o token + cluster_id). Ver DoNodePoolPicker.
663
+ digitalocean: [
664
+ {
665
+ key: 'api_token',
666
+ labelKey: 'fieldApiToken',
667
+ type: 'password',
668
+ required: true,
669
+ colSpan: true,
670
+ },
671
+ {
672
+ key: 'cluster_id',
673
+ labelKey: 'fieldClusterId',
674
+ type: 'text',
675
+ required: true,
676
+ },
677
+ {
678
+ key: 'region',
679
+ labelKey: 'fieldRegion',
680
+ type: 'text',
681
+ required: true,
682
+ placeholder: 'nyc1',
683
+ },
684
+ ],
685
+ kubernetes: [
686
+ {
687
+ key: 'api_server',
688
+ labelKey: 'fieldApiServer',
689
+ type: 'url',
690
+ required: true,
691
+ colSpan: true,
692
+ placeholder: 'https://10.0.0.1:6443',
693
+ },
694
+ {
695
+ key: 'token',
696
+ labelKey: 'fieldToken',
697
+ type: 'password',
698
+ required: true,
699
+ colSpan: true,
700
+ },
701
+ {
702
+ key: 'ca_cert',
703
+ labelKey: 'fieldCaCert',
704
+ type: 'password',
705
+ required: false,
706
+ colSpan: true,
707
+ },
708
+ {
709
+ key: 'namespace',
710
+ labelKey: 'fieldNamespace',
711
+ type: 'text',
712
+ required: false,
713
+ placeholder: 'hcode',
714
+ },
715
+ ],
656
716
  };
657
717
 
658
718
  // ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -722,6 +782,7 @@ function TypeTab({
722
782
  totalItems,
723
783
  search,
724
784
  setSearch,
785
+ isLoading,
725
786
  } = usePagination({ url: `/integration-profile?typeId=${typeId}` });
726
787
 
727
788
  useEffect(() => {
@@ -786,13 +847,7 @@ function TypeTab({
786
847
  placeholder={t('searchPlaceholder')}
787
848
  />
788
849
 
789
- {profiles.length === 0 ? (
790
- <EmptyState
791
- icon={<PlugZap className="h-12 w-12" />}
792
- title={t('noProfilesFound')}
793
- description={t('noProfilesHint')}
794
- />
795
- ) : (
850
+ {isLoading || profiles.length > 0 ? (
796
851
  <div className="rounded-md border">
797
852
  <Table>
798
853
  <TableHeader>
@@ -826,7 +881,10 @@ function TypeTab({
826
881
  </TableRow>
827
882
  </TableHeader>
828
883
  <TableBody>
829
- {profiles.map((profile) => (
884
+ {isLoading ? (
885
+ <TableBodySkeleton rows={pageSize} columns={9} />
886
+ ) : (
887
+ profiles.map((profile) => (
830
888
  <TableRow
831
889
  key={profile.id}
832
890
  className="cursor-pointer"
@@ -907,10 +965,17 @@ function TypeTab({
907
965
  </div>
908
966
  </TableCell>
909
967
  </TableRow>
910
- ))}
968
+ ))
969
+ )}
911
970
  </TableBody>
912
971
  </Table>
913
972
  </div>
973
+ ) : (
974
+ <EmptyState
975
+ icon={<PlugZap className="h-12 w-12" />}
976
+ title={t('noProfilesFound')}
977
+ description={t('noProfilesHint')}
978
+ />
914
979
  )}
915
980
 
916
981
  <PaginationFooter
@@ -1075,6 +1140,10 @@ export default function IntegrationProfilesPage() {
1075
1140
  {};
1076
1141
  const providerSlug =
1077
1142
  providers.find((p) => p.id === data.provider_id)?.slug ?? '';
1143
+ // Segredos chegam mascarados (********) do backend e PERMANECEM mascarados no
1144
+ // form: o campo fica desabilitado mostrando ********; o botão "trocar" limpa e
1145
+ // reabilita para digitar um novo valor. Em branco ou mascarado, o backend
1146
+ // preserva o segredo armazenado.
1078
1147
  if (
1079
1148
  providerSlug.endsWith('-oauth') &&
1080
1149
  !Array.isArray(loadedConfig.callback_urls)
@@ -1844,6 +1913,15 @@ export default function IntegrationProfilesPage() {
1844
1913
 
1845
1914
  const isPassword = field.type === 'password';
1846
1915
  const isVisible = visibleFields[field.key] ?? false;
1916
+ const currentValue = String(
1917
+ formData.config[field.key] ?? ''
1918
+ );
1919
+ // Segredo já armazenado: campo desabilitado mostrando ******** +
1920
+ // botão "trocar" que limpa o valor e reabilita para digitar um novo.
1921
+ const isLockedSecret =
1922
+ isPassword &&
1923
+ Boolean(editingProfile) &&
1924
+ currentValue === SECRET_MASK;
1847
1925
 
1848
1926
  return (
1849
1927
  <div
@@ -1865,49 +1943,76 @@ export default function IntegrationProfilesPage() {
1865
1943
  id={`ip-cfg-${field.key}`}
1866
1944
  type={
1867
1945
  isPassword
1868
- ? isVisible
1946
+ ? isVisible && !isLockedSecret
1869
1947
  ? 'text'
1870
1948
  : 'password'
1871
1949
  : field.type === 'number'
1872
1950
  ? 'number'
1873
1951
  : 'text'
1874
1952
  }
1875
- value={String(formData.config[field.key] ?? '')}
1953
+ value={currentValue}
1876
1954
  onChange={(e) =>
1877
1955
  updateConfig(field.key, e.target.value)
1878
1956
  }
1879
- placeholder={field.placeholder}
1880
- required={field.required}
1957
+ placeholder={
1958
+ isPassword && editingProfile
1959
+ ? t('leaveBlankToKeep')
1960
+ : field.placeholder
1961
+ }
1962
+ required={
1963
+ field.required &&
1964
+ !(isPassword && Boolean(editingProfile))
1965
+ }
1881
1966
  autoComplete={
1882
1967
  isPassword ? 'new-password' : undefined
1883
1968
  }
1884
1969
  className={isPassword ? 'pr-10' : undefined}
1970
+ disabled={isLockedSecret}
1885
1971
  />
1886
- {isPassword && (
1887
- <Button
1888
- type="button"
1889
- variant="ghost"
1890
- size="icon"
1891
- className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
1892
- onClick={() =>
1893
- setVisibleFields((p) => ({
1894
- ...p,
1895
- [field.key]: !p[field.key],
1896
- }))
1897
- }
1898
- aria-label={
1899
- isVisible
1900
- ? t('hidePassword')
1901
- : t('showPassword')
1902
- }
1903
- >
1904
- {isVisible ? (
1905
- <EyeOff className="h-4 w-4" />
1906
- ) : (
1907
- <Eye className="h-4 w-4" />
1908
- )}
1909
- </Button>
1910
- )}
1972
+ {isPassword &&
1973
+ (isLockedSecret ? (
1974
+ <Button
1975
+ type="button"
1976
+ variant="ghost"
1977
+ size="icon"
1978
+ className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
1979
+ onClick={() => {
1980
+ updateConfig(field.key, '');
1981
+ setVisibleFields((p) => ({
1982
+ ...p,
1983
+ [field.key]: true,
1984
+ }));
1985
+ }}
1986
+ aria-label={t('changeSecret')}
1987
+ title={t('changeSecret')}
1988
+ >
1989
+ <Pencil className="h-4 w-4" />
1990
+ </Button>
1991
+ ) : (
1992
+ <Button
1993
+ type="button"
1994
+ variant="ghost"
1995
+ size="icon"
1996
+ className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
1997
+ onClick={() =>
1998
+ setVisibleFields((p) => ({
1999
+ ...p,
2000
+ [field.key]: !p[field.key],
2001
+ }))
2002
+ }
2003
+ aria-label={
2004
+ isVisible
2005
+ ? t('hidePassword')
2006
+ : t('showPassword')
2007
+ }
2008
+ >
2009
+ {isVisible ? (
2010
+ <EyeOff className="h-4 w-4" />
2011
+ ) : (
2012
+ <Eye className="h-4 w-4" />
2013
+ )}
2014
+ </Button>
2015
+ ))}
1911
2016
  </div>
1912
2017
  </div>
1913
2018
  );
@@ -1916,6 +2021,19 @@ export default function IntegrationProfilesPage() {
1916
2021
  </div>
1917
2022
  )}
1918
2023
 
2024
+ {/* ── DigitalOcean: node pool de vídeo (escolha por nome via API) ── */}
2025
+ {selectedProvider?.slug === 'digitalocean' && (
2026
+ <DoNodePoolPicker
2027
+ apiToken={String(formData.config.api_token ?? '')}
2028
+ clusterId={String(formData.config.cluster_id ?? '')}
2029
+ value={String(formData.config.video_node_pool_name ?? '')}
2030
+ profileId={editingProfile?.id}
2031
+ onChange={(name) =>
2032
+ updateConfig('video_node_pool_name', name)
2033
+ }
2034
+ />
2035
+ )}
2036
+
1919
2037
  {/* ── Callback URLs (OAuth only) ── */}
1920
2038
  {isOAuthProvider && (
1921
2039
  <div className="rounded-lg border">