insforge 1.2.10 → 1.3.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 (335) hide show
  1. package/.claude-plugin/marketplace.json +20 -20
  2. package/.dockerignore +60 -60
  3. package/.env.example +83 -77
  4. package/.github/ISSUE_TEMPLATE/bug_report.yml +36 -36
  5. package/.github/ISSUE_TEMPLATE/config.yml +11 -11
  6. package/.github/ISSUE_TEMPLATE/feature_request.yml +26 -26
  7. package/.github/PULL_REQUEST_TEMPLATE.md +7 -7
  8. package/.github/copilot-instructions.md +146 -146
  9. package/.github/workflows/build-image.yml +65 -65
  10. package/.github/workflows/ci-premerge-check.yml +23 -23
  11. package/.github/workflows/e2e.yml +63 -63
  12. package/.github/workflows/lint-and-format.yml +32 -32
  13. package/.prettierignore +64 -64
  14. package/CHANGELOG.md +44 -44
  15. package/CLAUDE_PLUGIN.md +104 -104
  16. package/CODE_OF_CONDUCT.md +128 -128
  17. package/CONTRIBUTING.md +125 -125
  18. package/Dockerfile +30 -30
  19. package/GITHUB_OAUTH_SETUP.md +49 -49
  20. package/GOOGLE_OAUTH_SETUP.md +148 -148
  21. package/LICENSE +201 -201
  22. package/README.md +182 -182
  23. package/assets/Dark.svg +23 -23
  24. package/auth/package.json +28 -28
  25. package/auth/src/lib/broadcastService.ts +117 -115
  26. package/auth/src/pages/SignInPage.tsx +60 -57
  27. package/auth/src/pages/SignUpPage.tsx +60 -57
  28. package/auth/tsconfig.json +32 -32
  29. package/auth/tsconfig.node.json +11 -11
  30. package/backend/package.json +78 -75
  31. package/backend/src/api/routes/ai/index.routes.ts +3 -3
  32. package/backend/src/api/routes/auth/index.routes.ts +667 -570
  33. package/backend/src/api/routes/auth/oauth.routes.ts +473 -448
  34. package/backend/src/api/routes/database/advance.routes.ts +37 -16
  35. package/backend/src/api/routes/database/index.routes.ts +78 -1
  36. package/backend/src/api/routes/database/records.routes.ts +10 -10
  37. package/backend/src/api/routes/database/tables.routes.ts +0 -14
  38. package/backend/src/api/routes/docs/index.routes.ts +75 -76
  39. package/backend/src/api/routes/email/index.routes.ts +35 -0
  40. package/backend/src/api/routes/functions/index.routes.ts +18 -12
  41. package/backend/src/api/routes/metadata/index.routes.ts +12 -0
  42. package/backend/src/api/routes/realtime/channels.routes.ts +81 -0
  43. package/backend/src/api/routes/realtime/index.routes.ts +12 -0
  44. package/backend/src/api/routes/realtime/messages.routes.ts +48 -0
  45. package/backend/src/api/routes/realtime/permissions.routes.ts +19 -0
  46. package/backend/src/api/routes/storage/index.routes.ts +18 -12
  47. package/backend/src/api/routes/usage/index.routes.ts +6 -4
  48. package/backend/src/infra/database/database.manager.ts +14 -1
  49. package/backend/src/infra/database/migrations/000_create-base-tables.sql +141 -141
  50. package/backend/src/infra/database/migrations/001_create-helper-functions.sql +40 -40
  51. package/backend/src/infra/database/migrations/002_rename-auth-tables.sql +29 -29
  52. package/backend/src/infra/database/migrations/003_create-users-table.sql +55 -55
  53. package/backend/src/infra/database/migrations/004_add-reload-postgrest-func.sql +23 -23
  54. package/backend/src/infra/database/migrations/005_enable-project-admin-modify-users.sql +29 -29
  55. package/backend/src/infra/database/migrations/006_modify-ai-usage-table.sql +24 -24
  56. package/backend/src/infra/database/migrations/007_drop-metadata-table.sql +1 -1
  57. package/backend/src/infra/database/migrations/008_add-system-tables.sql +76 -76
  58. package/backend/src/infra/database/migrations/009_add-function-secrets.sql +23 -23
  59. package/backend/src/infra/database/migrations/010_modify-ai-config-modalities.sql +93 -93
  60. package/backend/src/infra/database/migrations/011_refactor-secrets-table.sql +15 -15
  61. package/backend/src/infra/database/migrations/012_add-storage-uploaded-by.sql +7 -7
  62. package/backend/src/infra/database/migrations/013_create-auth-schema-functions.sql +44 -44
  63. package/backend/src/infra/database/migrations/014_add-updated-at-trigger-user-table.sql +7 -7
  64. package/backend/src/infra/database/migrations/015_create-auth-config-and-email-otp-tables.sql +59 -59
  65. package/backend/src/infra/database/migrations/016_update-auth-config-and-email-otp.sql +24 -24
  66. package/backend/src/infra/database/migrations/017_create-realtime-schema.sql +233 -0
  67. package/backend/src/infra/realtime/realtime.manager.ts +246 -0
  68. package/backend/src/infra/realtime/webhook-sender.ts +82 -0
  69. package/backend/src/infra/security/token.manager.ts +219 -125
  70. package/backend/src/infra/socket/socket.manager.ts +198 -64
  71. package/backend/src/providers/ai/openrouter.provider.ts +12 -9
  72. package/backend/src/providers/email/base.provider.ts +4 -7
  73. package/backend/src/providers/email/cloud.provider.ts +84 -0
  74. package/backend/src/providers/oauth/apple.provider.ts +266 -0
  75. package/backend/src/providers/oauth/index.ts +1 -0
  76. package/backend/src/server.ts +317 -284
  77. package/backend/src/services/ai/ai-model.service.ts +5 -5
  78. package/backend/src/services/ai/chat-completion.service.ts +4 -4
  79. package/backend/src/services/ai/image-generation.service.ts +3 -3
  80. package/backend/src/services/auth/auth.service.ts +14 -0
  81. package/backend/src/services/database/database-table.service.ts +0 -9
  82. package/backend/src/services/database/database.service.ts +127 -0
  83. package/backend/src/services/email/email.service.ts +5 -7
  84. package/backend/src/services/realtime/index.ts +3 -0
  85. package/backend/src/services/realtime/realtime-auth.service.ts +104 -0
  86. package/backend/src/services/realtime/realtime-channel.service.ts +237 -0
  87. package/backend/src/services/realtime/realtime-message.service.ts +260 -0
  88. package/backend/src/types/auth.ts +11 -0
  89. package/backend/src/types/realtime.ts +18 -0
  90. package/backend/src/types/socket.ts +7 -31
  91. package/backend/src/utils/cookies.ts +35 -0
  92. package/backend/src/utils/s3-config-loader.ts +64 -0
  93. package/backend/src/utils/seed.ts +301 -298
  94. package/backend/src/utils/sql-parser.ts +90 -0
  95. package/backend/tests/README.md +133 -133
  96. package/backend/tests/cleanup-all-test-data.sh +230 -230
  97. package/backend/tests/cloud/test-s3-multitenant.sh +131 -131
  98. package/backend/tests/local/comprehensive-curl-tests.sh +155 -155
  99. package/backend/tests/local/test-ai-config.sh +129 -129
  100. package/backend/tests/local/test-ai-usage.sh +80 -80
  101. package/backend/tests/local/test-auth-router.sh +143 -143
  102. package/backend/tests/local/test-database-router.sh +222 -222
  103. package/backend/tests/local/test-e2e.sh +240 -240
  104. package/backend/tests/local/test-fk-errors.sh +96 -96
  105. package/backend/tests/local/test-functions.sh +123 -123
  106. package/backend/tests/local/test-id-field.sh +200 -200
  107. package/backend/tests/local/test-logs.sh +132 -132
  108. package/backend/tests/local/test-public-bucket.sh +264 -264
  109. package/backend/tests/local/test-secrets.sh +249 -249
  110. package/backend/tests/local/test-serverless-functions.sh.disabled +325 -325
  111. package/backend/tests/local/test-traditional-rest.sh +208 -208
  112. package/backend/tests/manual/README.md +50 -50
  113. package/backend/tests/manual/create-large-table-simple.sql +10 -10
  114. package/backend/tests/manual/seed-large-table.sql +100 -100
  115. package/backend/tests/manual/setup-large-table-extras.sql +33 -33
  116. package/backend/tests/manual/test-bulk-upsert.sh +409 -409
  117. package/backend/tests/manual/test-database-advance.sh +296 -296
  118. package/backend/tests/manual/test-postgrest-stability.sh +191 -191
  119. package/backend/tests/manual/test-rawsql-export-import.sh +411 -411
  120. package/backend/tests/manual/test-rawsql-modes.sh +244 -244
  121. package/backend/tests/manual/test-universal-storage.sh +263 -263
  122. package/backend/tests/manual/test-users.sql +17 -17
  123. package/backend/tests/run-all-tests.sh +139 -139
  124. package/backend/tests/setup.ts +0 -0
  125. package/backend/tests/test-config.sh +338 -338
  126. package/backend/tests/unit/analyze-query.test.ts +697 -0
  127. package/backend/tsconfig.json +22 -22
  128. package/claude-plugin/.claude-plugin/plugin.json +24 -24
  129. package/claude-plugin/README.md +133 -133
  130. package/claude-plugin/skills/insforge-schema-patterns/SKILL.md +270 -270
  131. package/docker-compose.prod.yml +204 -200
  132. package/docker-compose.yml +232 -228
  133. package/docker-init/db/db-init.sql +97 -97
  134. package/docker-init/db/jwt.sql +5 -5
  135. package/docker-init/db/postgresql.conf +16 -16
  136. package/docker-init/logs/vector.yml +236 -236
  137. package/docs/README.md +44 -44
  138. package/docs/agent-docs/real-time.md +269 -0
  139. package/docs/changelog.mdx +119 -67
  140. package/docs/core-concepts/ai/architecture.mdx +372 -372
  141. package/docs/core-concepts/ai/sdk.mdx +213 -213
  142. package/docs/core-concepts/authentication/architecture.mdx +278 -278
  143. package/docs/core-concepts/authentication/sdk.mdx +414 -414
  144. package/docs/core-concepts/authentication/ui-components/customization.mdx +529 -529
  145. package/docs/core-concepts/authentication/ui-components/nextjs.mdx +221 -221
  146. package/docs/core-concepts/authentication/ui-components/react-router.mdx +184 -184
  147. package/docs/core-concepts/authentication/ui-components/react.mdx +129 -129
  148. package/docs/core-concepts/database/architecture.mdx +255 -255
  149. package/docs/core-concepts/database/sdk.mdx +382 -382
  150. package/docs/core-concepts/email/architecture.mdx +101 -0
  151. package/docs/core-concepts/email/sdk.mdx +53 -0
  152. package/docs/core-concepts/functions/architecture.mdx +105 -105
  153. package/docs/core-concepts/functions/sdk.mdx +184 -184
  154. package/docs/core-concepts/realtime/architecture.mdx +446 -0
  155. package/docs/core-concepts/realtime/sdk.mdx +409 -0
  156. package/docs/core-concepts/storage/architecture.mdx +243 -243
  157. package/docs/core-concepts/storage/sdk.mdx +253 -253
  158. package/docs/deployment/README.md +94 -94
  159. package/docs/deployment/deploy-to-aws-ec2.md +564 -564
  160. package/docs/deployment/deploy-to-azure-virtual-machines.md +312 -312
  161. package/docs/deployment/deploy-to-google-cloud-compute-engine.md +613 -613
  162. package/docs/deployment/deploy-to-render.md +441 -441
  163. package/docs/deprecated/insforge-auth-api.md +214 -214
  164. package/docs/deprecated/insforge-auth-sdk.md +99 -99
  165. package/docs/deprecated/insforge-db-api.md +358 -358
  166. package/docs/deprecated/insforge-db-sdk.md +139 -139
  167. package/docs/deprecated/insforge-debug-sdk.md +156 -156
  168. package/docs/deprecated/insforge-debug.md +64 -64
  169. package/docs/deprecated/insforge-instructions.md +123 -123
  170. package/docs/deprecated/insforge-project.md +117 -117
  171. package/docs/deprecated/insforge-storage-api.md +278 -278
  172. package/docs/deprecated/insforge-storage-sdk.md +158 -158
  173. package/docs/docs.json +232 -210
  174. package/docs/examples/framework-guides/nextjs.mdx +131 -131
  175. package/docs/examples/framework-guides/nuxt.mdx +165 -165
  176. package/docs/examples/framework-guides/react.mdx +165 -165
  177. package/docs/examples/framework-guides/svelte.mdx +153 -153
  178. package/docs/examples/framework-guides/vue.mdx +159 -159
  179. package/docs/examples/overview.mdx +67 -67
  180. package/docs/favicon.svg +19 -19
  181. package/docs/images/changelog/dec-2025/ai-integration.png +0 -0
  182. package/docs/images/changelog/dec-2025/ai-models.webp +0 -0
  183. package/docs/images/changelog/dec-2025/alipay-payment.webp +0 -0
  184. package/docs/images/changelog/dec-2025/apple-login.jpg +0 -0
  185. package/docs/images/changelog/dec-2025/mcp-installer.png +0 -0
  186. package/docs/images/changelog/dec-2025/realtime-module.jpg +0 -0
  187. package/docs/images/icons/ai.svg +4 -4
  188. package/docs/images/logos/nextjs.svg +4 -4
  189. package/docs/images/logos/nuxt.svg +4 -4
  190. package/docs/images/logos/react.svg +5 -5
  191. package/docs/images/logos/svelte.svg +4 -4
  192. package/docs/images/logos/vue.svg +5 -5
  193. package/docs/insforge-instructions-sdk.md +89 -88
  194. package/docs/introduction.mdx +45 -45
  195. package/docs/logo/dark.svg +22 -22
  196. package/docs/logo/light.svg +20 -20
  197. package/docs/partnership.mdx +651 -646
  198. package/docs/quickstart.mdx +82 -82
  199. package/docs/showcase.mdx +52 -52
  200. package/docs/snippets/sdk-installation.mdx +21 -21
  201. package/docs/snippets/service-icons.mdx +27 -27
  202. package/examples/oauth/frontend-oauth-example.html +250 -250
  203. package/examples/response-examples.md +443 -443
  204. package/frontend/components.json +17 -17
  205. package/frontend/package.json +69 -69
  206. package/frontend/src/assets/icons/checkbox_checked.svg +6 -6
  207. package/frontend/src/assets/icons/checkbox_undetermined.svg +6 -6
  208. package/frontend/src/assets/icons/checked.svg +3 -3
  209. package/frontend/src/assets/icons/connected.svg +3 -3
  210. package/frontend/src/assets/icons/error.svg +3 -3
  211. package/frontend/src/assets/icons/loader.svg +9 -9
  212. package/frontend/src/assets/icons/pencil.svg +4 -4
  213. package/frontend/src/assets/icons/refresh.svg +4 -4
  214. package/frontend/src/assets/icons/step_active.svg +3 -3
  215. package/frontend/src/assets/icons/step_inactive.svg +11 -11
  216. package/frontend/src/assets/icons/warning.svg +3 -3
  217. package/frontend/src/assets/logos/apple.svg +3 -3
  218. package/frontend/src/assets/logos/claude_code.svg +3 -3
  219. package/frontend/src/assets/logos/cline.svg +6 -6
  220. package/frontend/src/assets/logos/cursor.svg +20 -20
  221. package/frontend/src/assets/logos/discord.svg +8 -8
  222. package/frontend/src/assets/logos/facebook.svg +3 -3
  223. package/frontend/src/assets/logos/gemini.svg +19 -19
  224. package/frontend/src/assets/logos/github.svg +5 -5
  225. package/frontend/src/assets/logos/google.svg +13 -13
  226. package/frontend/src/assets/logos/grok.svg +10 -10
  227. package/frontend/src/assets/logos/insforge_dark.svg +15 -15
  228. package/frontend/src/assets/logos/insforge_light.svg +15 -15
  229. package/frontend/src/assets/logos/instagram.svg +1 -1
  230. package/frontend/src/assets/logos/linkedin.svg +3 -3
  231. package/frontend/src/assets/logos/openai.svg +10 -10
  232. package/frontend/src/assets/logos/roo_code.svg +9 -9
  233. package/frontend/src/assets/logos/spotify.svg +16 -16
  234. package/frontend/src/assets/logos/tiktok.svg +5 -5
  235. package/frontend/src/assets/logos/trae.svg +3 -3
  236. package/frontend/src/assets/logos/windsurf.svg +10 -10
  237. package/frontend/src/assets/logos/x.svg +3 -3
  238. package/frontend/src/components/layout/AppHeader.tsx +9 -10
  239. package/frontend/src/features/auth/components/OAuthConfigDialog.tsx +1 -0
  240. package/frontend/src/features/auth/components/UsersDataGrid.tsx +6 -0
  241. package/frontend/src/features/auth/helpers.tsx +8 -0
  242. package/frontend/src/features/auth/{page → pages}/UsersPage.tsx +0 -28
  243. package/frontend/src/features/database/components/SQLModal.tsx +75 -0
  244. package/frontend/src/features/database/components/TableForm.tsx +0 -4
  245. package/frontend/src/features/database/hooks/useDatabase.ts +66 -0
  246. package/frontend/src/features/database/hooks/useTables.ts +32 -28
  247. package/frontend/src/features/database/index.ts +1 -0
  248. package/frontend/src/features/database/{page → pages}/FunctionsPage.tsx +29 -37
  249. package/frontend/src/features/database/{page → pages}/IndexesPage.tsx +35 -47
  250. package/frontend/src/features/database/{page → pages}/PoliciesPage.tsx +43 -54
  251. package/frontend/src/features/database/{page → pages}/TablesPage.tsx +0 -42
  252. package/frontend/src/features/database/{page → pages}/TriggersPage.tsx +35 -47
  253. package/frontend/src/features/database/services/advance.service.ts +0 -26
  254. package/frontend/src/features/database/services/database.service.ts +55 -0
  255. package/frontend/src/features/database/services/table.service.ts +0 -6
  256. package/frontend/src/features/functions/{page → pages}/FunctionsPage.tsx +21 -44
  257. package/frontend/src/features/functions/{page → pages}/SecretsPage.tsx +11 -9
  258. package/frontend/src/features/logs/hooks/useMcpUsage.ts +13 -66
  259. package/frontend/src/features/realtime/components/ChannelRow.tsx +83 -0
  260. package/frontend/src/features/realtime/components/EditChannelModal.tsx +246 -0
  261. package/frontend/src/features/realtime/components/MessageRow.tsx +85 -0
  262. package/frontend/src/features/realtime/components/RealtimeEmptyState.tsx +30 -0
  263. package/frontend/src/features/realtime/hooks/useRealtime.ts +218 -0
  264. package/frontend/src/features/realtime/index.ts +11 -0
  265. package/frontend/src/features/realtime/pages/RealtimeChannelsPage.tsx +172 -0
  266. package/frontend/src/features/realtime/pages/RealtimeMessagesPage.tsx +211 -0
  267. package/frontend/src/features/realtime/pages/RealtimePermissionsPage.tsx +191 -0
  268. package/frontend/src/features/realtime/services/realtime.service.ts +107 -0
  269. package/frontend/src/features/storage/{page → pages}/StoragePage.tsx +1 -29
  270. package/frontend/src/features/visualizer/components/SchemaVisualizer.tsx +3 -3
  271. package/frontend/src/features/visualizer/{page → pages}/VisualizerPage.tsx +1 -35
  272. package/frontend/src/lib/contexts/SocketContext.tsx +119 -75
  273. package/frontend/src/lib/routing/AppRoutes.tsx +35 -20
  274. package/frontend/src/lib/utils/cloudMessaging.ts +1 -1
  275. package/frontend/src/lib/utils/menuItems.ts +24 -0
  276. package/frontend/src/lib/utils/utils.ts +14 -1
  277. package/frontend/tsconfig.json +25 -25
  278. package/frontend/tsconfig.node.json +9 -9
  279. package/functions/deno.json +24 -24
  280. package/functions/server.ts +315 -315
  281. package/i18n/README.ar.md +130 -130
  282. package/i18n/README.de.md +130 -130
  283. package/i18n/README.es.md +154 -154
  284. package/i18n/README.fr.md +134 -134
  285. package/i18n/README.hi.md +129 -129
  286. package/i18n/README.ja.md +174 -174
  287. package/i18n/README.ko.md +136 -136
  288. package/i18n/README.pt-BR.md +131 -131
  289. package/i18n/README.ru.md +129 -129
  290. package/i18n/README.zh-CN.md +133 -133
  291. package/openapi/ai.yaml +715 -715
  292. package/openapi/auth.yaml +1244 -1244
  293. package/openapi/email.yaml +158 -0
  294. package/openapi/functions.yaml +475 -475
  295. package/openapi/health.yaml +29 -29
  296. package/openapi/logs.yaml +223 -223
  297. package/openapi/metadata.yaml +177 -177
  298. package/openapi/realtime.yaml +699 -0
  299. package/openapi/records.yaml +381 -381
  300. package/openapi/secrets.yaml +370 -370
  301. package/openapi/storage.yaml +875 -875
  302. package/openapi/tables.yaml +463 -463
  303. package/package.json +97 -97
  304. package/shared-schemas/package.json +31 -31
  305. package/shared-schemas/src/ai.schema.ts +63 -59
  306. package/shared-schemas/src/auth-api.schema.ts +352 -339
  307. package/shared-schemas/src/auth.schema.ts +1 -1
  308. package/shared-schemas/src/database-api.schema.ts +32 -1
  309. package/shared-schemas/src/database.schema.ts +39 -0
  310. package/shared-schemas/src/docs.schema.ts +26 -0
  311. package/shared-schemas/src/email-api.schema.ts +30 -0
  312. package/shared-schemas/src/index.ts +4 -0
  313. package/shared-schemas/src/metadata.schema.ts +9 -0
  314. package/shared-schemas/src/realtime-api.schema.ts +111 -0
  315. package/shared-schemas/src/realtime.schema.ts +143 -0
  316. package/shared-schemas/tsconfig.json +21 -21
  317. package/tsconfig.json +7 -7
  318. package/zeabur/README.md +13 -13
  319. package/zeabur/template.yml +1032 -1032
  320. package/.cursor/rules/cursor-rules.mdc +0 -94
  321. package/frontend/src/features/database/hooks/useFullMetadata.ts +0 -18
  322. package/test-gemini.sh +0 -35
  323. package/test-usage-admin.sh +0 -57
  324. package/test-usage.sh +0 -50
  325. /package/frontend/src/features/ai/{page → pages}/AIPage.tsx +0 -0
  326. /package/frontend/src/features/auth/{page → pages}/AuthMethodsPage.tsx +0 -0
  327. /package/frontend/src/features/auth/{page → pages}/ConfigurationPage.tsx +0 -0
  328. /package/frontend/src/features/dashboard/{page → pages}/DashboardPage.tsx +0 -0
  329. /package/frontend/src/features/database/{page → pages}/SQLEditorPage.tsx +0 -0
  330. /package/frontend/src/features/database/{page → pages}/TemplatesPage.tsx +0 -0
  331. /package/frontend/src/features/login/{page → pages}/CloudLoginPage.tsx +0 -0
  332. /package/frontend/src/features/login/{page → pages}/LoginPage.tsx +0 -0
  333. /package/frontend/src/features/logs/{page → pages}/AuditsPage.tsx +0 -0
  334. /package/frontend/src/features/logs/{page → pages}/LogsPage.tsx +0 -0
  335. /package/frontend/src/features/logs/{page → pages}/MCPLogsPage.tsx +0 -0
@@ -0,0 +1,172 @@
1
+ import { useState } from 'react';
2
+ import RefreshIcon from '@/assets/icons/refresh.svg?react';
3
+ import {
4
+ Button,
5
+ ConfirmDialog,
6
+ Skeleton,
7
+ Tooltip,
8
+ TooltipContent,
9
+ TooltipProvider,
10
+ TooltipTrigger,
11
+ } from '@/components';
12
+ import { useConfirm } from '@/lib/hooks/useConfirm';
13
+ import { useRealtime } from '../hooks/useRealtime';
14
+ import { ChannelRow } from '../components/ChannelRow';
15
+ import { EditChannelModal } from '../components/EditChannelModal';
16
+ import RealtimeEmptyState from '../components/RealtimeEmptyState';
17
+ import type { RealtimeChannel } from '../services/realtime.service';
18
+
19
+ export default function RealtimeChannelsPage() {
20
+ const [isRefreshing, setIsRefreshing] = useState(false);
21
+ const [selectedChannel, setSelectedChannel] = useState<RealtimeChannel | null>(null);
22
+ const [isModalOpen, setIsModalOpen] = useState(false);
23
+
24
+ const {
25
+ channels,
26
+ isLoadingChannels,
27
+ refetchChannels,
28
+ updateChannel,
29
+ isUpdating,
30
+ deleteChannel,
31
+ isDeleting,
32
+ } = useRealtime();
33
+
34
+ const { confirm, confirmDialogProps } = useConfirm();
35
+
36
+ const handleRefresh = async () => {
37
+ setIsRefreshing(true);
38
+ try {
39
+ await refetchChannels();
40
+ } finally {
41
+ setIsRefreshing(false);
42
+ }
43
+ };
44
+
45
+ const handleRowClick = (channel: RealtimeChannel) => {
46
+ setSelectedChannel(channel);
47
+ setIsModalOpen(true);
48
+ };
49
+
50
+ const handleToggleEnabled = (channel: RealtimeChannel, enabled: boolean) => {
51
+ updateChannel({ id: channel.id, data: { enabled } });
52
+ };
53
+
54
+ const handleDelete = async (channel: RealtimeChannel) => {
55
+ const shouldDelete = await confirm({
56
+ title: 'Delete Channel',
57
+ description: `Are you sure you want to delete the channel "${channel.pattern}"? This action cannot be undone.`,
58
+ confirmText: 'Delete',
59
+ destructive: true,
60
+ });
61
+
62
+ if (shouldDelete) {
63
+ deleteChannel(channel.id);
64
+ }
65
+ };
66
+
67
+ const handleModalSave = (id: string, data: Parameters<typeof updateChannel>[0]['data']) => {
68
+ updateChannel(
69
+ { id, data },
70
+ {
71
+ onSuccess: () => {
72
+ setIsModalOpen(false);
73
+ setSelectedChannel(null);
74
+ },
75
+ }
76
+ );
77
+ };
78
+ return (
79
+ <div className="h-full flex flex-col overflow-hidden">
80
+ <div className="flex flex-col gap-6 p-4">
81
+ <div className="flex items-center gap-3">
82
+ <h1 className="text-xl font-normal text-zinc-950 dark:text-white">Channels</h1>
83
+
84
+ {/* Separator */}
85
+ <div className="h-6 w-px bg-gray-200 dark:bg-neutral-700" />
86
+
87
+ {/* Refresh button */}
88
+ <TooltipProvider>
89
+ <Tooltip>
90
+ <TooltipTrigger asChild>
91
+ <Button
92
+ variant="ghost"
93
+ size="icon"
94
+ className="p-1 h-9 w-9"
95
+ onClick={() => void handleRefresh()}
96
+ disabled={isRefreshing}
97
+ >
98
+ <RefreshIcon className="h-5 w-5 text-zinc-400 dark:text-neutral-400" />
99
+ </Button>
100
+ </TooltipTrigger>
101
+ <TooltipContent side="bottom" align="center">
102
+ <p>{isRefreshing ? 'Refreshing...' : 'Refresh'}</p>
103
+ </TooltipContent>
104
+ </Tooltip>
105
+ </TooltipProvider>
106
+ </div>
107
+
108
+ {/* Table Header */}
109
+ <div className="flex items-center pl-3 pr-[44px] text-sm text-muted-foreground dark:text-neutral-400">
110
+ <div className="w-[76px] shrink-0 py-1 px-3">Enabled</div>
111
+ <div className="flex-1 py-1 px-3">Pattern</div>
112
+ <div className="w-[640px] py-1 px-3">Description</div>
113
+ <div className="flex-1 py-1 px-3">Created</div>
114
+ </div>
115
+ </div>
116
+
117
+ {/* Scrollable Table Body */}
118
+ <div className="flex-1 min-h-0 overflow-y-auto px-4 pb-4 relative">
119
+ <div className="flex flex-col gap-2">
120
+ {isLoadingChannels ? (
121
+ <>
122
+ {[...Array(4)].map((_, i) => (
123
+ <Skeleton key={i} className="h-14 rounded-[8px]" />
124
+ ))}
125
+ </>
126
+ ) : channels.length >= 1 ? (
127
+ <>
128
+ {channels.map((channel) => (
129
+ <ChannelRow
130
+ key={channel.id}
131
+ channel={channel}
132
+ onClick={() => handleRowClick(channel)}
133
+ onToggleEnabled={(enabled) => handleToggleEnabled(channel, enabled)}
134
+ onDelete={() => void handleDelete(channel)}
135
+ isUpdating={isUpdating}
136
+ isDeleting={isDeleting}
137
+ />
138
+ ))}
139
+ </>
140
+ ) : (
141
+ <RealtimeEmptyState type="channels" />
142
+ )}
143
+ </div>
144
+
145
+ {/* Loading mask overlay */}
146
+ {isRefreshing && (
147
+ <div className="absolute inset-0 bg-white dark:bg-neutral-800 flex items-center justify-center z-50">
148
+ <div className="flex items-center gap-1">
149
+ <div className="w-5 h-5 border-2 border-zinc-500 dark:border-neutral-700 border-t-transparent rounded-full animate-spin" />
150
+ <span className="text-sm text-zinc-500 dark:text-zinc-400">Loading</span>
151
+ </div>
152
+ </div>
153
+ )}
154
+ </div>
155
+
156
+ <EditChannelModal
157
+ channel={selectedChannel}
158
+ open={isModalOpen}
159
+ onOpenChange={(open) => {
160
+ setIsModalOpen(open);
161
+ if (!open) {
162
+ setSelectedChannel(null);
163
+ }
164
+ }}
165
+ onSave={handleModalSave}
166
+ isUpdating={isUpdating}
167
+ />
168
+
169
+ <ConfirmDialog {...confirmDialogProps} />
170
+ </div>
171
+ );
172
+ }
@@ -0,0 +1,211 @@
1
+ import { useState } from 'react';
2
+ import { ChevronRight } from 'lucide-react';
3
+ import RefreshIcon from '@/assets/icons/refresh.svg?react';
4
+ import {
5
+ Button,
6
+ PaginationControls,
7
+ Skeleton,
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from '@/components';
13
+ import { useRealtime } from '../hooks/useRealtime';
14
+ import { MessageRow } from '../components/MessageRow';
15
+ import RealtimeEmptyState from '../components/RealtimeEmptyState';
16
+ import type { RealtimeMessage } from '../services/realtime.service';
17
+
18
+ export default function RealtimeMessagesPage() {
19
+ const [isRefreshing, setIsRefreshing] = useState(false);
20
+ const [selectedMessage, setSelectedMessage] = useState<RealtimeMessage | null>(null);
21
+
22
+ const {
23
+ messages,
24
+ isLoadingMessages,
25
+ refetchMessages,
26
+ messagesPageSize,
27
+ messagesCurrentPage,
28
+ messagesTotalCount,
29
+ messagesTotalPages,
30
+ setMessagesPage,
31
+ } = useRealtime();
32
+
33
+ const handleRefresh = async () => {
34
+ setIsRefreshing(true);
35
+ try {
36
+ await refetchMessages();
37
+ } finally {
38
+ setIsRefreshing(false);
39
+ }
40
+ };
41
+
42
+ // Message detail view
43
+ if (selectedMessage) {
44
+ return (
45
+ <div className="h-full flex flex-col overflow-hidden">
46
+ <div className="flex items-center gap-2.5 p-4 border-b border-border-gray dark:border-neutral-600">
47
+ <button
48
+ onClick={() => setSelectedMessage(null)}
49
+ className="text-xl text-zinc-500 dark:text-neutral-400 hover:text-zinc-950 dark:hover:text-white transition-colors"
50
+ >
51
+ Messages
52
+ </button>
53
+ <ChevronRight className="w-5 h-5 text-muted-foreground dark:text-neutral-400" />
54
+ <p className="text-xl text-zinc-950 dark:text-white">{selectedMessage.eventName}</p>
55
+ </div>
56
+
57
+ <div className="flex-1 min-h-0 p-4 overflow-auto">
58
+ <div className="space-y-4">
59
+ <div className="grid grid-cols-3 gap-4">
60
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
61
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-1">Channel</p>
62
+ <p className="text-sm text-zinc-950 dark:text-white">
63
+ {selectedMessage.channelName}
64
+ </p>
65
+ </div>
66
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
67
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-1">
68
+ Sender Type
69
+ </p>
70
+ <p className="text-sm text-zinc-950 dark:text-white">
71
+ {selectedMessage.senderType}
72
+ </p>
73
+ </div>
74
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
75
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-1">Created</p>
76
+ <p className="text-sm text-zinc-950 dark:text-white">{selectedMessage.createdAt}</p>
77
+ </div>
78
+ </div>
79
+
80
+ <div className="grid grid-cols-3 gap-4">
81
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
82
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-1">
83
+ WS Audience
84
+ </p>
85
+ <p className="text-sm text-zinc-950 dark:text-white">
86
+ {selectedMessage.wsAudienceCount}
87
+ </p>
88
+ </div>
89
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
90
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-1">
91
+ WH Audience
92
+ </p>
93
+ <p className="text-sm text-zinc-950 dark:text-white">
94
+ {selectedMessage.whAudienceCount}
95
+ </p>
96
+ </div>
97
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
98
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-1">
99
+ WH Delivered
100
+ </p>
101
+ <p className="text-sm text-zinc-950 dark:text-white">
102
+ {selectedMessage.whDeliveredCount}
103
+ </p>
104
+ </div>
105
+ </div>
106
+
107
+ <div className="p-4 rounded-lg bg-neutral-100 dark:bg-[#333333]">
108
+ <p className="text-sm text-muted-foreground dark:text-neutral-400 mb-2">Payload</p>
109
+ <pre className="text-sm text-zinc-950 dark:text-white font-mono whitespace-pre-wrap overflow-auto">
110
+ {JSON.stringify(selectedMessage.payload, null, 2)}
111
+ </pre>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ // Default list view
120
+ return (
121
+ <div className="h-full flex flex-col overflow-hidden">
122
+ {/* Fixed Page Header */}
123
+ <div className="shrink-0 flex items-center gap-3 p-4 pb-0">
124
+ <h1 className="text-xl font-normal text-zinc-950 dark:text-white">Messages</h1>
125
+
126
+ {/* Separator */}
127
+ <div className="h-6 w-px bg-gray-200 dark:bg-neutral-700" />
128
+
129
+ {/* Refresh button */}
130
+ <TooltipProvider>
131
+ <Tooltip>
132
+ <TooltipTrigger asChild>
133
+ <Button
134
+ variant="ghost"
135
+ size="icon"
136
+ className="p-1 h-9 w-9"
137
+ onClick={() => void handleRefresh()}
138
+ disabled={isRefreshing}
139
+ >
140
+ <RefreshIcon className="h-5 w-5 text-zinc-400 dark:text-neutral-400" />
141
+ </Button>
142
+ </TooltipTrigger>
143
+ <TooltipContent side="bottom" align="center">
144
+ <p>{isRefreshing ? 'Refreshing...' : 'Refresh'}</p>
145
+ </TooltipContent>
146
+ </Tooltip>
147
+ </TooltipProvider>
148
+ </div>
149
+
150
+ {/* Fixed Table Header */}
151
+ <div className="shrink-0 grid grid-cols-12 px-7 pt-6 pb-2 text-sm text-muted-foreground dark:text-neutral-400">
152
+ <div className="col-span-2 py-1 px-3">Event</div>
153
+ <div className="col-span-2 py-1 px-3">Channel</div>
154
+ <div className="col-span-1 py-1 px-3">Sender</div>
155
+ <div className="col-span-3 py-1 px-3">Payload</div>
156
+ <div className="col-span-1 py-1 px-3">WebSockets</div>
157
+ <div className="col-span-1 py-1 px-3">Webhooks</div>
158
+ <div className="col-span-2 py-1 px-3">Sent At</div>
159
+ </div>
160
+
161
+ {/* Scrollable Content Area */}
162
+ <div className="flex-1 min-h-0 overflow-auto px-4 pb-4 relative">
163
+ <div className="flex flex-col gap-2">
164
+ {isLoadingMessages ? (
165
+ <>
166
+ {[...Array(4)].map((_, i) => (
167
+ <Skeleton key={i} className="h-14 rounded-[8px]" />
168
+ ))}
169
+ </>
170
+ ) : messages.length >= 1 ? (
171
+ <>
172
+ {messages.map((message) => (
173
+ <MessageRow
174
+ key={message.id}
175
+ message={message}
176
+ onClick={() => setSelectedMessage(message)}
177
+ />
178
+ ))}
179
+ </>
180
+ ) : (
181
+ <RealtimeEmptyState type="messages" />
182
+ )}
183
+ </div>
184
+
185
+ {/* Loading mask overlay */}
186
+ {isRefreshing && (
187
+ <div className="absolute inset-0 bg-white dark:bg-neutral-800 flex items-center justify-center z-50">
188
+ <div className="flex items-center gap-1">
189
+ <div className="w-5 h-5 border-2 border-zinc-500 dark:border-neutral-700 border-t-transparent rounded-full animate-spin" />
190
+ <span className="text-sm text-zinc-500 dark:text-zinc-400">Loading</span>
191
+ </div>
192
+ </div>
193
+ )}
194
+ </div>
195
+
196
+ {/* Pagination */}
197
+ {messages.length > 0 && (
198
+ <div className="shrink-0">
199
+ <PaginationControls
200
+ currentPage={messagesCurrentPage}
201
+ totalPages={messagesTotalPages}
202
+ onPageChange={setMessagesPage}
203
+ totalRecords={messagesTotalCount}
204
+ pageSize={messagesPageSize}
205
+ recordLabel="messages"
206
+ />
207
+ </div>
208
+ )}
209
+ </div>
210
+ );
211
+ }
@@ -0,0 +1,191 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ DataGrid,
4
+ type ConvertedValue,
5
+ type DataGridColumn,
6
+ type DataGridRowType,
7
+ EmptyState,
8
+ } from '@/components';
9
+ import { SQLModal, SQLCellButton } from '@/features/database';
10
+ import { useRealtime } from '../hooks/useRealtime';
11
+ import type { RlsPolicy } from '../services/realtime.service';
12
+ import { cn } from '@/lib/utils/utils';
13
+
14
+ type TabType = 'subscribe' | 'publish';
15
+
16
+ interface PolicyRow extends DataGridRowType {
17
+ id: string;
18
+ policyName: string;
19
+ command: string;
20
+ roles: string;
21
+ using: string | null;
22
+ withCheck: string | null;
23
+ [key: string]: ConvertedValue | { [key: string]: string }[];
24
+ }
25
+
26
+ function mapPoliciesToRows(policies: RlsPolicy[]): PolicyRow[] {
27
+ return policies.map((policy, index) => ({
28
+ id: `${policy.tableName}_${policy.policyName}_${index}`,
29
+ policyName: policy.policyName,
30
+ command: policy.command === '*' ? 'ALL' : policy.command,
31
+ roles: Array.isArray(policy.roles) ? policy.roles.join(', ') : String(policy.roles),
32
+ using: policy.using,
33
+ withCheck: policy.withCheck,
34
+ }));
35
+ }
36
+
37
+ export default function RealtimePermissionsPage() {
38
+ const [activeTab, setActiveTab] = useState<TabType>('subscribe');
39
+ const [sqlModal, setSqlModal] = useState({ open: false, title: '', value: '' });
40
+
41
+ const { permissions, isLoadingPermissions: isLoading, permissionsError: error } = useRealtime();
42
+
43
+ const subscribePolicies = useMemo(
44
+ () => (permissions ? mapPoliciesToRows(permissions.subscribe.policies) : []),
45
+ [permissions]
46
+ );
47
+
48
+ const publishPolicies = useMemo(
49
+ () => (permissions ? mapPoliciesToRows(permissions.publish.policies) : []),
50
+ [permissions]
51
+ );
52
+
53
+ const activePolicies = activeTab === 'subscribe' ? subscribePolicies : publishPolicies;
54
+
55
+ const columns: DataGridColumn<PolicyRow>[] = useMemo(
56
+ () => [
57
+ {
58
+ key: 'policyName',
59
+ name: 'Policy Name',
60
+ width: 'minmax(200px, 2fr)',
61
+ resizable: true,
62
+ sortable: true,
63
+ },
64
+ {
65
+ key: 'command',
66
+ name: 'Command',
67
+ width: 'minmax(100px, 1fr)',
68
+ resizable: true,
69
+ sortable: true,
70
+ renderCell: ({ row }) => {
71
+ return (
72
+ <span className="inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-slate-600 text-white">
73
+ {row.command}
74
+ </span>
75
+ );
76
+ },
77
+ },
78
+ {
79
+ key: 'roles',
80
+ name: 'Roles',
81
+ width: 'minmax(150px, 1.5fr)',
82
+ resizable: true,
83
+ },
84
+ {
85
+ key: 'using',
86
+ name: 'Using',
87
+ width: 'minmax(200px, 2fr)',
88
+ resizable: true,
89
+ renderCell: ({ row }) => (
90
+ <SQLCellButton
91
+ value={row.using}
92
+ onClick={() =>
93
+ row.using && setSqlModal({ open: true, title: 'Using', value: row.using })
94
+ }
95
+ />
96
+ ),
97
+ },
98
+ {
99
+ key: 'withCheck',
100
+ name: 'With Check',
101
+ width: 'minmax(200px, 2fr)',
102
+ resizable: true,
103
+ renderCell: ({ row }) => (
104
+ <SQLCellButton
105
+ value={row.withCheck}
106
+ onClick={() =>
107
+ row.withCheck &&
108
+ setSqlModal({ open: true, title: 'With Check', value: row.withCheck })
109
+ }
110
+ />
111
+ ),
112
+ },
113
+ ],
114
+ [setSqlModal]
115
+ );
116
+
117
+ if (error) {
118
+ return (
119
+ <div className="flex-1 flex items-center justify-center">
120
+ <EmptyState
121
+ title="Failed to load permissions"
122
+ description={error instanceof Error ? error.message : 'An error occurred'}
123
+ />
124
+ </div>
125
+ );
126
+ }
127
+
128
+ return (
129
+ <div className="h-full flex flex-col overflow-hidden">
130
+ {/* Fixed Header */}
131
+ <div className="shrink-0 bg-bg-gray dark:bg-neutral-800 p-4 flex flex-col gap-6">
132
+ {/* Title */}
133
+ <h1 className="text-xl font-normal text-zinc-950 dark:text-white">Permissions</h1>
134
+
135
+ {/* Tabs */}
136
+ <div className="flex gap-6 items-start">
137
+ <button
138
+ onClick={() => setActiveTab('subscribe')}
139
+ className={cn(
140
+ 'h-8 text-sm font-medium transition-colors',
141
+ activeTab === 'subscribe'
142
+ ? 'text-zinc-950 dark:text-white border-b-2 border-zinc-950 dark:border-white'
143
+ : 'text-zinc-500 dark:text-neutral-400 hover:text-zinc-700 dark:hover:text-neutral-300'
144
+ )}
145
+ >
146
+ Subscribe Policies
147
+ </button>
148
+ <button
149
+ onClick={() => setActiveTab('publish')}
150
+ className={cn(
151
+ 'h-8 text-sm font-medium transition-colors',
152
+ activeTab === 'publish'
153
+ ? 'text-zinc-950 dark:text-white border-b-2 border-zinc-950 dark:border-white'
154
+ : 'text-zinc-500 dark:text-neutral-400 hover:text-zinc-700 dark:hover:text-neutral-300'
155
+ )}
156
+ >
157
+ Publish Policies
158
+ </button>
159
+ </div>
160
+ </div>
161
+
162
+ {/* Content */}
163
+ <div className="flex-1 min-h-0 overflow-hidden px-3 pb-2">
164
+ {isLoading ? (
165
+ <div className="flex items-center justify-center h-full">
166
+ <EmptyState title="Loading policies..." description="Please wait" />
167
+ </div>
168
+ ) : (
169
+ <DataGrid
170
+ data={activePolicies}
171
+ columns={columns}
172
+ showSelection={false}
173
+ showPagination={false}
174
+ noPadding={true}
175
+ emptyState={
176
+ <div className="text-sm text-zinc-500 dark:text-zinc-400">No policies defined</div>
177
+ }
178
+ />
179
+ )}
180
+ </div>
181
+
182
+ {/* SQL Detail Modal */}
183
+ <SQLModal
184
+ open={sqlModal.open}
185
+ onOpenChange={(open) => setSqlModal((prev) => ({ ...prev, open }))}
186
+ title={sqlModal.title}
187
+ value={sqlModal.value}
188
+ />
189
+ </div>
190
+ );
191
+ }
@@ -0,0 +1,107 @@
1
+ import { apiClient } from '@/lib/api/client';
2
+ import type {
3
+ RealtimeChannel,
4
+ RealtimeMessage,
5
+ CreateChannelRequest,
6
+ UpdateChannelRequest,
7
+ ListMessagesRequest,
8
+ MessageStatsResponse,
9
+ RlsPolicy,
10
+ RealtimePermissionsResponse,
11
+ } from '@insforge/shared-schemas';
12
+
13
+ export type { RealtimeChannel, RealtimeMessage, RlsPolicy, RealtimePermissionsResponse };
14
+
15
+ export class RealtimeService {
16
+ // ============================================================================
17
+ // Channels
18
+ // ============================================================================
19
+
20
+ async listChannels(): Promise<RealtimeChannel[]> {
21
+ return apiClient.request('/realtime/channels', {
22
+ headers: apiClient.withAccessToken(),
23
+ });
24
+ }
25
+
26
+ async getChannel(id: string): Promise<RealtimeChannel> {
27
+ return apiClient.request(`/realtime/channels/${id}`, {
28
+ headers: apiClient.withAccessToken(),
29
+ });
30
+ }
31
+
32
+ async createChannel(data: CreateChannelRequest): Promise<RealtimeChannel> {
33
+ return apiClient.request('/realtime/channels', {
34
+ method: 'POST',
35
+ headers: apiClient.withAccessToken(),
36
+ body: JSON.stringify(data),
37
+ });
38
+ }
39
+
40
+ async updateChannel(id: string, data: UpdateChannelRequest): Promise<RealtimeChannel> {
41
+ return apiClient.request(`/realtime/channels/${id}`, {
42
+ method: 'PUT',
43
+ headers: apiClient.withAccessToken(),
44
+ body: JSON.stringify(data),
45
+ });
46
+ }
47
+
48
+ async deleteChannel(id: string): Promise<void> {
49
+ return apiClient.request(`/realtime/channels/${id}`, {
50
+ method: 'DELETE',
51
+ headers: apiClient.withAccessToken(),
52
+ });
53
+ }
54
+
55
+ // ============================================================================
56
+ // Messages
57
+ // ============================================================================
58
+
59
+ async listMessages(params?: ListMessagesRequest): Promise<RealtimeMessage[]> {
60
+ const searchParams = new URLSearchParams();
61
+ if (params?.channelId) {
62
+ searchParams.set('channelId', params.channelId);
63
+ }
64
+ if (params?.eventName) {
65
+ searchParams.set('eventName', params.eventName);
66
+ }
67
+ if (params?.limit) {
68
+ searchParams.set('limit', String(params.limit));
69
+ }
70
+ if (params?.offset) {
71
+ searchParams.set('offset', String(params.offset));
72
+ }
73
+
74
+ const query = searchParams.toString();
75
+ const endpoint = `/realtime/messages${query ? `?${query}` : ''}`;
76
+
77
+ return apiClient.request(endpoint, {
78
+ headers: apiClient.withAccessToken(),
79
+ });
80
+ }
81
+
82
+ async getMessageStats(channelId?: string): Promise<MessageStatsResponse> {
83
+ const searchParams = new URLSearchParams();
84
+ if (channelId) {
85
+ searchParams.set('channelId', channelId);
86
+ }
87
+
88
+ const query = searchParams.toString();
89
+ const endpoint = `/realtime/messages/stats${query ? `?${query}` : ''}`;
90
+
91
+ return apiClient.request(endpoint, {
92
+ headers: apiClient.withAccessToken(),
93
+ });
94
+ }
95
+
96
+ // ============================================================================
97
+ // Permissions
98
+ // ============================================================================
99
+
100
+ async getPermissions(): Promise<RealtimePermissionsResponse> {
101
+ return apiClient.request('/realtime/permissions', {
102
+ headers: apiClient.withAccessToken(),
103
+ });
104
+ }
105
+ }
106
+
107
+ export const realtimeService = new RealtimeService();