@gokiteam/goki-dev 0.2.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 (205) hide show
  1. package/README.md +478 -0
  2. package/bin/goki-dev.js +452 -0
  3. package/bin/mcp-server.js +16 -0
  4. package/bin/secrets-cli.js +302 -0
  5. package/cli/ComposeOverrideGenerator.js +226 -0
  6. package/cli/ComposeParser.js +73 -0
  7. package/cli/ConfigGenerator.js +304 -0
  8. package/cli/ConfigManager.js +46 -0
  9. package/cli/DatabaseManager.js +94 -0
  10. package/cli/DevToolsChecker.js +21 -0
  11. package/cli/DevToolsDir.js +66 -0
  12. package/cli/DevToolsManager.js +451 -0
  13. package/cli/DockerManager.js +138 -0
  14. package/cli/FunctionManager.js +95 -0
  15. package/cli/HttpProxyRewriter.js +91 -0
  16. package/cli/Logger.js +10 -0
  17. package/cli/McpConfigManager.js +123 -0
  18. package/cli/NgrokManager.js +431 -0
  19. package/cli/ProjectCLI.js +2322 -0
  20. package/cli/PubSubManager.js +129 -0
  21. package/cli/SnapshotManager.js +88 -0
  22. package/cli/UiFormatter.js +292 -0
  23. package/cli/WebhookUrlRewriter.js +32 -0
  24. package/cli/secrets/BiometricAuth.js +125 -0
  25. package/cli/secrets/SecretInjector.js +47 -0
  26. package/cli/secrets/SecretsConfig.js +141 -0
  27. package/cli/secrets/SecretsDoctor.js +384 -0
  28. package/cli/secrets/SecretsManager.js +255 -0
  29. package/client/dist/client.d.ts +332 -0
  30. package/client/dist/client.js +507 -0
  31. package/client/dist/helpers.d.ts +62 -0
  32. package/client/dist/helpers.js +122 -0
  33. package/client/dist/index.d.ts +59 -0
  34. package/client/dist/index.js +78 -0
  35. package/client/dist/package.json +1 -0
  36. package/client/dist/types.d.ts +280 -0
  37. package/client/dist/types.js +7 -0
  38. package/config.development +46 -0
  39. package/config.test +18 -0
  40. package/guidelines/CodingStyleGuideline.md +148 -0
  41. package/guidelines/CommentingGuideline.md +10 -0
  42. package/guidelines/HttpApiImplementationGuideline.md +137 -0
  43. package/guidelines/NamingGuideline.md +182 -0
  44. package/package.json +138 -0
  45. package/patterns/api/[collectionName]/Controllers.md +62 -0
  46. package/patterns/api/[collectionName]/Logic.md +154 -0
  47. package/patterns/api/[collectionName]/Permissions.md +81 -0
  48. package/patterns/api/[collectionName]/Router.md +83 -0
  49. package/patterns/api/[collectionName]/Schemas.md +197 -0
  50. package/patterns/configs/Patterns.md +7 -0
  51. package/patterns/enums/Patterns.md +24 -0
  52. package/patterns/errorHandling/Patterns.md +185 -0
  53. package/patterns/testing/Patterns.md +232 -0
  54. package/src/Server.js +238 -0
  55. package/src/api/dashboard/Controllers.js +9 -0
  56. package/src/api/dashboard/Logic.js +76 -0
  57. package/src/api/dashboard/Router.js +11 -0
  58. package/src/api/dashboard/Schemas.js +47 -0
  59. package/src/api/data/Controllers.js +26 -0
  60. package/src/api/data/Logic.js +188 -0
  61. package/src/api/data/Router.js +16 -0
  62. package/src/api/docker/Controllers.js +33 -0
  63. package/src/api/docker/Logic.js +268 -0
  64. package/src/api/docker/Router.js +15 -0
  65. package/src/api/docker/Schemas.js +80 -0
  66. package/src/api/docs/Controllers.js +15 -0
  67. package/src/api/docs/Logic.js +85 -0
  68. package/src/api/docs/Router.js +12 -0
  69. package/src/api/export/Controllers.js +30 -0
  70. package/src/api/export/Logic.js +143 -0
  71. package/src/api/export/Router.js +18 -0
  72. package/src/api/export/Schemas.js +104 -0
  73. package/src/api/firestore/Controllers.js +152 -0
  74. package/src/api/firestore/Logic.js +474 -0
  75. package/src/api/firestore/Router.js +23 -0
  76. package/src/api/functions/Controllers.js +261 -0
  77. package/src/api/functions/Logic.js +710 -0
  78. package/src/api/functions/Router.js +50 -0
  79. package/src/api/functions/Schemas.js +193 -0
  80. package/src/api/gateway/Controllers.js +72 -0
  81. package/src/api/gateway/Logic.js +74 -0
  82. package/src/api/gateway/Router.js +10 -0
  83. package/src/api/gateway/Schemas.js +19 -0
  84. package/src/api/health/Controllers.js +14 -0
  85. package/src/api/health/Logic.js +24 -0
  86. package/src/api/health/Router.js +12 -0
  87. package/src/api/httpTraffic/Controllers.js +29 -0
  88. package/src/api/httpTraffic/Logic.js +33 -0
  89. package/src/api/httpTraffic/Router.js +9 -0
  90. package/src/api/httpTraffic/Schemas.js +23 -0
  91. package/src/api/logging/Controllers.js +80 -0
  92. package/src/api/logging/Logic.js +461 -0
  93. package/src/api/logging/Router.js +24 -0
  94. package/src/api/logging/Schemas.js +43 -0
  95. package/src/api/mqtt/Controllers.js +17 -0
  96. package/src/api/mqtt/Logic.js +66 -0
  97. package/src/api/mqtt/Router.js +12 -0
  98. package/src/api/postgres/Controllers.js +97 -0
  99. package/src/api/postgres/Logic.js +221 -0
  100. package/src/api/postgres/Router.js +21 -0
  101. package/src/api/pubsub/Controllers.js +236 -0
  102. package/src/api/pubsub/Logic.js +732 -0
  103. package/src/api/pubsub/Router.js +41 -0
  104. package/src/api/pubsub/Schemas.js +355 -0
  105. package/src/api/redis/Controllers.js +63 -0
  106. package/src/api/redis/Logic.js +239 -0
  107. package/src/api/redis/Router.js +21 -0
  108. package/src/api/scheduler/Controllers.js +27 -0
  109. package/src/api/scheduler/Logic.js +49 -0
  110. package/src/api/scheduler/Router.js +16 -0
  111. package/src/api/services/Controllers.js +26 -0
  112. package/src/api/services/Logic.js +205 -0
  113. package/src/api/services/Router.js +14 -0
  114. package/src/api/services/Schemas.js +66 -0
  115. package/src/api/snapshots/Controllers.js +37 -0
  116. package/src/api/snapshots/Logic.js +797 -0
  117. package/src/api/snapshots/Router.js +15 -0
  118. package/src/api/snapshots/Schemas.js +23 -0
  119. package/src/api/webhooks/Controllers.js +49 -0
  120. package/src/api/webhooks/Logic.js +137 -0
  121. package/src/api/webhooks/Router.js +12 -0
  122. package/src/api/webhooks/Schemas.js +31 -0
  123. package/src/configs/Application.js +147 -0
  124. package/src/configs/Default.js +13 -0
  125. package/src/consumers/BlackboxLogsConsumer.js +235 -0
  126. package/src/consumers/DockerLogsConsumer.js +687 -0
  127. package/src/db/Tables.js +66 -0
  128. package/src/db/schemas/firestore.js +18 -0
  129. package/src/db/schemas/functions.js +65 -0
  130. package/src/db/schemas/httpTraffic.js +43 -0
  131. package/src/db/schemas/logging.js +74 -0
  132. package/src/db/schemas/migrations.js +64 -0
  133. package/src/db/schemas/mqtt.js +56 -0
  134. package/src/db/schemas/pubsub.js +90 -0
  135. package/src/db/schemas/pubsubRegistry.js +22 -0
  136. package/src/db/schemas/webhooks.js +28 -0
  137. package/src/emulation/awsiot/Controllers.js +91 -0
  138. package/src/emulation/awsiot/Logic.js +70 -0
  139. package/src/emulation/awsiot/Router.js +19 -0
  140. package/src/emulation/awsiot/Server.js +100 -0
  141. package/src/emulation/firestore/Server.js +136 -0
  142. package/src/emulation/logging/Controllers.js +212 -0
  143. package/src/emulation/logging/Logic.js +416 -0
  144. package/src/emulation/logging/Router.js +36 -0
  145. package/src/emulation/logging/Schemas.js +82 -0
  146. package/src/emulation/logging/Server.js +108 -0
  147. package/src/emulation/pubsub/Controllers.js +279 -0
  148. package/src/emulation/pubsub/DefaultTopics.js +162 -0
  149. package/src/emulation/pubsub/Logic.js +427 -0
  150. package/src/emulation/pubsub/README.md +309 -0
  151. package/src/emulation/pubsub/Router.js +33 -0
  152. package/src/emulation/pubsub/Server.js +104 -0
  153. package/src/emulation/pubsub/ShadowPoller.js +276 -0
  154. package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
  155. package/src/enums/ContainerNames.js +106 -0
  156. package/src/enums/ErrorReason.js +28 -0
  157. package/src/enums/FunctionStatuses.js +15 -0
  158. package/src/enums/FunctionTriggerTypes.js +15 -0
  159. package/src/enums/GatewayState.js +7 -0
  160. package/src/enums/ServiceNames.js +68 -0
  161. package/src/jobs/DatabaseMaintenance.js +184 -0
  162. package/src/jobs/MessageHistoryCleanup.js +152 -0
  163. package/src/mcp/ApiClient.js +25 -0
  164. package/src/mcp/Server.js +52 -0
  165. package/src/mcp/prompts/debugging.js +104 -0
  166. package/src/mcp/resources/platform.js +118 -0
  167. package/src/mcp/tools/data.js +84 -0
  168. package/src/mcp/tools/docker.js +166 -0
  169. package/src/mcp/tools/firestore.js +162 -0
  170. package/src/mcp/tools/functions.js +380 -0
  171. package/src/mcp/tools/httpTraffic.js +69 -0
  172. package/src/mcp/tools/logging.js +174 -0
  173. package/src/mcp/tools/mqtt.js +37 -0
  174. package/src/mcp/tools/postgres.js +130 -0
  175. package/src/mcp/tools/pubsub.js +316 -0
  176. package/src/mcp/tools/redis.js +146 -0
  177. package/src/mcp/tools/services.js +169 -0
  178. package/src/mcp/tools/snapshots.js +88 -0
  179. package/src/mcp/tools/webhooks.js +115 -0
  180. package/src/middleware/DevProxy.js +67 -0
  181. package/src/middleware/ErrorCatcher.js +35 -0
  182. package/src/middleware/HttpProxy.js +215 -0
  183. package/src/middleware/Reply.js +24 -0
  184. package/src/middleware/TraceId.js +9 -0
  185. package/src/middleware/WebhookProxy.js +234 -0
  186. package/src/protocols/mqtt/Broker.js +92 -0
  187. package/src/protocols/mqtt/Handlers.js +175 -0
  188. package/src/protocols/mqtt/PubSubBridge.js +162 -0
  189. package/src/protocols/mqtt/Server.js +116 -0
  190. package/src/runtime/FunctionRunner.js +179 -0
  191. package/src/services/AppGatewayService.js +582 -0
  192. package/src/singletons/FirestoreBroadcaster.js +367 -0
  193. package/src/singletons/FunctionTriggerDispatcher.js +456 -0
  194. package/src/singletons/FunctionsService.js +418 -0
  195. package/src/singletons/HttpProxy.js +224 -0
  196. package/src/singletons/LogBroadcaster.js +159 -0
  197. package/src/singletons/Logger.js +49 -0
  198. package/src/singletons/MemoryJsonStore.js +175 -0
  199. package/src/singletons/MessageBroadcaster.js +190 -0
  200. package/src/singletons/PostgresBroadcaster.js +367 -0
  201. package/src/singletons/PostgresClient.js +180 -0
  202. package/src/singletons/RedisClient.js +184 -0
  203. package/src/singletons/SqliteStore.js +480 -0
  204. package/src/singletons/TickService.js +151 -0
  205. package/src/singletons/WebhookProxy.js +223 -0
@@ -0,0 +1,232 @@
1
+ # Testing Patterns
2
+
3
+ ## Test structure
4
+
5
+ Coding pattern:
6
+ ```javascript
7
+ import { describe, it, beforeEach } from 'mocha'
8
+ import { expect } from 'chai'
9
+
10
+ describe('Feature Name', () => {
11
+ beforeEach(async function () {
12
+ const { id } = this.currentTest.ctx
13
+ this.testId = id
14
+ })
15
+
16
+ describe('Happy paths', () => {
17
+ it('should work with valid input', async function () {
18
+ // Test implementation
19
+ })
20
+ })
21
+
22
+ describe('Extended positive tests', () => {
23
+ // Additional success cases
24
+ })
25
+
26
+ describe('Edge cases', () => {
27
+ // Boundary conditions
28
+ })
29
+
30
+ describe('Validation', () => {
31
+ // Invalid input handling
32
+ })
33
+ })
34
+ ```
35
+
36
+ ## API endpoint testing
37
+
38
+ Coding pattern:
39
+ ```javascript
40
+ import { describe, it, beforeEach } from 'mocha'
41
+ import Chai, { expect } from 'chai'
42
+ import ChaiHttp from 'chai-http'
43
+ import { Application } from '../../../configs/Application.js'
44
+
45
+ Chai.use(ChaiHttp)
46
+
47
+ const endpoint = {
48
+ method: 'post',
49
+ url: '/v1/devices/create'
50
+ }
51
+
52
+ describe(`Testing API endpoint: ${endpoint.method.toUpperCase()} ${endpoint.url}`, () => {
53
+ beforeEach(async function () {
54
+ const { id } = this.currentTest.ctx
55
+ this.request = () => Chai.request(Application.server.base)[endpoint.method](endpoint.url)
56
+ .set('test-id', id)
57
+ })
58
+
59
+ it('should create device successfully', async function () {
60
+ const response = await this.request().send({
61
+ propertyId: 'prop-123',
62
+ model: 1
63
+ })
64
+ expect(response).to.have.status(200)
65
+ expect(response.body.device).to.exist
66
+ })
67
+
68
+ it('should reject missing required field', async function () {
69
+ const response = await this.request().send({
70
+ model: 1
71
+ // Missing propertyId
72
+ })
73
+ expect(response).to.have.status(400)
74
+ })
75
+ })
76
+ ```
77
+
78
+ ## Pub/Sub subscriber testing
79
+
80
+ Coding pattern:
81
+ ```javascript
82
+ import { describe, it, beforeEach } from 'mocha'
83
+ import { expect } from 'chai'
84
+ import { PubSubSubscriberProbe } from '@gokiteam/lab'
85
+ import { Application } from '../../../configs/Application.js'
86
+
87
+ describe('EventReceived Subscriber', () => {
88
+ beforeEach(async function () {
89
+ const { id } = this.currentTest.ctx
90
+ this.testId = id
91
+ this.probe = new PubSubSubscriberProbe({
92
+ projectId: Application.pubSub.projectId,
93
+ keyFilename: Application.pubSub.keyFilename
94
+ })
95
+ })
96
+
97
+ it('should process event successfully', async function () {
98
+ const topicName = Application.pubSub.subscribers.eventReceived.subscriptionName
99
+ await this.probe.publishMessage({
100
+ topicName,
101
+ message: {
102
+ deviceId: 'test-device-id',
103
+ action: 'update'
104
+ },
105
+ attributes: { traceId: this.testId }
106
+ })
107
+ const result = await this.probe.waitForResult({
108
+ topicName,
109
+ traceId: this.testId,
110
+ timeoutMilliseconds: 5000
111
+ })
112
+ expect(result.state).to.equal('processedSuccessfully')
113
+ })
114
+ })
115
+ ```
116
+
117
+ ## Logic function testing
118
+
119
+ ### Direct testing
120
+ ```javascript
121
+ import { describe, it } from 'mocha'
122
+ import { expect } from 'chai'
123
+ import { PrepareDeviceStatusRecord } from '../../../src/logic/PrepareDeviceStatusRecord.js'
124
+
125
+ describe('PrepareDeviceStatusRecord', () => {
126
+ it('should prepare device status record', () => {
127
+ const device = { doc: { propertyId: 'prop-123' } }
128
+ const deviceStatus = { doc: { batteryStatus: { percentage: 75 } } }
129
+ const deviceCurrentTimestamp = 1234567890
130
+ const result = PrepareDeviceStatusRecord({
131
+ device,
132
+ deviceStatus,
133
+ deviceCurrentTimestamp
134
+ })
135
+ expect(result).to.be.instanceOf(Buffer)
136
+ })
137
+ })
138
+ ```
139
+
140
+ ### Via HTTP endpoint
141
+ ```javascript
142
+ describe('ValidateDeviceControlOperation', () => {
143
+ beforeEach(async function () {
144
+ const { id } = this.currentTest.ctx
145
+ this.request = () => Chai.request(Application.server.base)
146
+ .post('/logic/ValidateDeviceControlOperation')
147
+ .set('test-id', id)
148
+ })
149
+
150
+ it('should validate unlock operation', async function () {
151
+ const response = await this.request().send({
152
+ key: { /* key data */ },
153
+ device: { /* device data */ },
154
+ deviceStatus: { /* status data */ },
155
+ deviceCurrentTimestamp: 1234567890,
156
+ isLockOperation: false
157
+ })
158
+ expect(response).to.have.status(200)
159
+ expect(response.body.errorCode).to.be.null
160
+ })
161
+ })
162
+ ```
163
+
164
+ ## Common assertions
165
+
166
+ ### Status codes
167
+ ```javascript
168
+ expect(response).to.have.status(200)
169
+ expect(response).to.have.status(400)
170
+ expect(response).to.have.status(404)
171
+ ```
172
+
173
+ ### Response body
174
+ ```javascript
175
+ expect(response.body).to.exist
176
+ expect(response.body.device).to.have.property('macAddress')
177
+ expect(response.body.device.macAddress).to.be.a('string')
178
+ ```
179
+
180
+ ### Arrays
181
+ ```javascript
182
+ expect(response.body.devices).to.be.an('array')
183
+ expect(response.body.devices).to.have.lengthOf(5)
184
+ ```
185
+
186
+ ### Pattern matching
187
+ ```javascript
188
+ expect(response.body.device.macAddress).to.match(/^([0-9A-F]{2}:){5}[0-9A-F]{2}$/)
189
+ ```
190
+
191
+ ## Test data management
192
+
193
+ ### Using collections
194
+ ```javascript
195
+ beforeEach(async function () {
196
+ const { id } = this.currentTest.ctx
197
+ const { collections } = this
198
+ this.device = await collections.devices.create({
199
+ propertyId: 'prop-123',
200
+ macAddress: '00:11:22:33:44:55',
201
+ model: 1
202
+ })
203
+ this.deviceStatus = await collections.deviceStatuses.create({
204
+ deviceId: this.device.id
205
+ })
206
+ })
207
+ ```
208
+
209
+ ## Running tests
210
+
211
+ ```bash
212
+ # All tests
213
+ npm test
214
+
215
+ # Parallel execution
216
+ npm run test:parallel
217
+
218
+ # API tests only
219
+ npm test:api
220
+
221
+ # Logic tests only
222
+ npm test:logic
223
+
224
+ # Pub/Sub tests only
225
+ npm test:pubsub
226
+
227
+ # Specific test file
228
+ npm test -- tests/component/app/devices/create.js
229
+
230
+ # Specific test by name
231
+ npm test -- --grep "should create device"
232
+ ```
package/src/Server.js ADDED
@@ -0,0 +1,238 @@
1
+ import Koa from 'koa'
2
+ import bodyParser from 'koa-bodyparser'
3
+ import KoaRouter from 'koa-router'
4
+ import cors from '@koa/cors'
5
+ import { Application } from './configs/Application.js'
6
+ import { Logger } from './singletons/Logger.js'
7
+ import { SqliteStore } from './singletons/SqliteStore.js'
8
+ import { ErrorCatcher } from './middleware/ErrorCatcher.js'
9
+ import { Reply } from './middleware/Reply.js'
10
+ import { TraceId } from './middleware/TraceId.js'
11
+ import { Router as HealthRouter } from './api/health/Router.js'
12
+ import { Router as DashboardRouter } from './api/dashboard/Router.js'
13
+ import { Router as ServicesRouter } from './api/services/Router.js'
14
+ import { Router as ExportRouter } from './api/export/Router.js'
15
+ import { Router as PubsubRouter } from './api/pubsub/Router.js'
16
+ import { Router as LoggingRouter } from './api/logging/Router.js'
17
+ import { Router as MqttRouter } from './api/mqtt/Router.js'
18
+ import { Router as DataRouter } from './api/data/Router.js'
19
+ import { Router as FirestoreRouter } from './api/firestore/Router.js'
20
+ import { Router as PostgresRouter } from './api/postgres/Router.js'
21
+ import { PostgresClient } from './singletons/PostgresClient.js'
22
+ import { RedisClient } from './singletons/RedisClient.js'
23
+ import { Router as RedisRouter } from './api/redis/Router.js'
24
+ import { Router as SchedulerRouter } from './api/scheduler/Router.js'
25
+ import { Router as GatewayRouter } from './api/gateway/Router.js'
26
+ import { Router as DockerRouter } from './api/docker/Router.js'
27
+ import { Router as DocsRouter } from './api/docs/Router.js'
28
+ import { Router as WebhooksRouter } from './api/webhooks/Router.js'
29
+ import { Router as HttpTrafficRouter } from './api/httpTraffic/Router.js'
30
+ import { Router as SnapshotsRouter } from './api/snapshots/Router.js'
31
+ import { Router as FunctionsRouter } from './api/functions/Router.js'
32
+ import { WebhookProxy } from './singletons/WebhookProxy.js'
33
+ import { HttpProxy } from './singletons/HttpProxy.js'
34
+ import { WebhookProxyMiddleware } from './middleware/WebhookProxy.js'
35
+ import { HttpProxyMiddleware } from './middleware/HttpProxy.js'
36
+ import { MqttServer } from './protocols/mqtt/Server.js'
37
+ import { start as startLoggingServer } from './emulation/logging/Server.js'
38
+ import { startPubSubServer } from './emulation/pubsub/Server.js'
39
+ import { startAwsIotServer } from './emulation/awsiot/Server.js'
40
+ import { startCleanupJob } from './jobs/MessageHistoryCleanup.js'
41
+ import { startMaintenanceJob } from './jobs/DatabaseMaintenance.js'
42
+ import { initializeDefaultTopics } from './emulation/pubsub/DefaultTopics.js'
43
+ import { BlackboxLogsConsumer } from './consumers/BlackboxLogsConsumer.js'
44
+ import { DockerLogsConsumer } from './consumers/DockerLogsConsumer.js'
45
+ import { AppGatewayService } from './services/AppGatewayService.js'
46
+ import { FunctionsService } from './singletons/FunctionsService.js'
47
+ import { FunctionTriggerDispatcher } from './singletons/FunctionTriggerDispatcher.js'
48
+
49
+ const { environment, runId, ports } = Application
50
+
51
+ const start = async () => {
52
+ try {
53
+ SqliteStore.initialize()
54
+ WebhookProxy.initialize()
55
+ HttpProxy.initialize()
56
+ PostgresClient.initialize()
57
+ RedisClient.initialize()
58
+ const app = new Koa()
59
+ app.use(cors({
60
+ origin: '*', // Allow all origins in development
61
+ credentials: true,
62
+ allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
63
+ allowHeaders: ['Content-Type', 'Authorization', 'X-Trace-Id']
64
+ }))
65
+ app.use(ErrorCatcher())
66
+ app.use(TraceId())
67
+ app.use(WebhookProxyMiddleware())
68
+ app.use(HttpProxyMiddleware())
69
+ app.use(Reply())
70
+ app.use(bodyParser())
71
+ const router = new KoaRouter()
72
+ router.use(HealthRouter.routes())
73
+ router.use(DashboardRouter.routes())
74
+ router.use(ServicesRouter.routes())
75
+ router.use(ExportRouter.routes())
76
+ router.use(PubsubRouter.routes())
77
+ router.use(LoggingRouter.routes())
78
+ router.use(MqttRouter.routes())
79
+ router.use(DataRouter.routes())
80
+ router.use(FirestoreRouter.routes())
81
+ router.use(PostgresRouter.routes())
82
+ router.use(RedisRouter.routes())
83
+ router.use(SchedulerRouter.routes())
84
+ router.use(GatewayRouter.routes())
85
+ router.use(DockerRouter.routes())
86
+ router.use(DocsRouter.routes())
87
+ router.use(WebhooksRouter.routes())
88
+ router.use(HttpTrafficRouter.routes())
89
+ router.use(SnapshotsRouter.routes())
90
+ router.use(FunctionsRouter.routes())
91
+ app.use(router.routes())
92
+ app.use(router.allowedMethods())
93
+
94
+ // Frontend is served separately by goki-dev-tools-frontend container (nginx)
95
+ // Backend only handles API routes starting with /v1
96
+ Logger.log({
97
+ level: 'info',
98
+ message: 'Backend server ready - API endpoints only (frontend served separately)'
99
+ })
100
+ const server = app.listen(ports.webUi, () => {
101
+ Logger.log({
102
+ level: 'info',
103
+ message: `Server started on port ${ports.webUi}`,
104
+ data: { environment, runId, port: ports.webUi }
105
+ })
106
+ })
107
+
108
+ // Start MQTT broker
109
+ MqttServer.start()
110
+
111
+ // Start AWS IoT Core HTTPS API emulation server
112
+ await startAwsIotServer()
113
+
114
+ // Start Pub/Sub emulation server
115
+ await startPubSubServer()
116
+
117
+ // Initialize default Pub/Sub topics (systemOneMinuteTick, etc.)
118
+ await initializeDefaultTopics()
119
+
120
+ // Start Cloud Logging emulation server
121
+ await startLoggingServer()
122
+
123
+ // Initialize Bull queue consumer for logs
124
+ const redisConfigs = []
125
+ redisConfigs.push({
126
+ host: Application.redis.host,
127
+ port: Application.redis.port,
128
+ db: Application.redis.db
129
+ })
130
+ if (
131
+ Application.redisLogs.host !== Application.redis.host ||
132
+ Application.redisLogs.port !== Application.redis.port
133
+ ) {
134
+ redisConfigs.push({
135
+ host: Application.redisLogs.host,
136
+ port: Application.redisLogs.port,
137
+ db: Application.redisLogs.db
138
+ })
139
+ Logger.log({
140
+ level: 'info',
141
+ message: 'Using separate Redis instance for logs',
142
+ data: {
143
+ dataRedis: `${Application.redis.host}:${Application.redis.port}`,
144
+ logsRedis: `${Application.redisLogs.host}:${Application.redisLogs.port}`
145
+ }
146
+ })
147
+ } else {
148
+ Logger.log({
149
+ level: 'info',
150
+ message: 'Using shared Redis instance for data and logs',
151
+ data: { redis: `${Application.redis.host}:${Application.redis.port}` }
152
+ })
153
+ }
154
+ global.logsConsumer = new BlackboxLogsConsumer(redisConfigs)
155
+
156
+ // Start Docker console log capture — all containers on goki-network
157
+ global.dockerLogsConsumer = new DockerLogsConsumer({
158
+ enabled: Application.logging?.dockerCapture !== false,
159
+ networkName: Application.logging?.dockerNetwork || 'goki-network',
160
+ ...(Application.logging?.dockerExcludedContainers && {
161
+ excludedContainers: Application.logging.dockerExcludedContainers
162
+ })
163
+ })
164
+ await global.dockerLogsConsumer.start()
165
+
166
+ // Start message history cleanup job
167
+ if (Application.messageHistory.enabled) {
168
+ startCleanupJob()
169
+ }
170
+
171
+ // Start database maintenance job (table trimming, WAL checkpoint, ANALYZE)
172
+ startMaintenanceJob()
173
+
174
+ // Auto-start App Gateway (non-blocking)
175
+ AppGatewayService.autoStart().catch(error => {
176
+ Logger.log({
177
+ level: 'error',
178
+ message: 'Gateway auto-start error',
179
+ data: { error: error.message }
180
+ })
181
+ })
182
+
183
+ // Initialize Cloud Functions service
184
+ await FunctionsService.initialize()
185
+ await FunctionTriggerDispatcher.start()
186
+
187
+ return server
188
+ } catch (error) {
189
+ Logger.log({
190
+ level: 'error',
191
+ message: 'Failed to start server',
192
+ data: { error: error.message, stack: error.stack }
193
+ })
194
+ process.exit(1)
195
+ }
196
+ }
197
+
198
+ const shutdown = async () => {
199
+ Logger.log({
200
+ level: 'info',
201
+ message: 'Shutting down server'
202
+ })
203
+ await AppGatewayService.stop()
204
+ FunctionTriggerDispatcher.stop()
205
+ await FunctionsService.shutdown()
206
+ MqttServer.stop()
207
+ if (global.logsConsumer) {
208
+ await global.logsConsumer.close()
209
+ }
210
+ if (global.dockerLogsConsumer) {
211
+ await global.dockerLogsConsumer.stop()
212
+ }
213
+ await PostgresClient.shutdown()
214
+ await RedisClient.shutdown()
215
+ SqliteStore.shutdown()
216
+ process.exit(0)
217
+ }
218
+
219
+ start()
220
+
221
+ process.on('SIGINT', shutdown)
222
+ process.on('SIGTERM', shutdown)
223
+
224
+ process.on('unhandledRejection', error => {
225
+ Logger.log({
226
+ level: 'error',
227
+ message: 'Unhandled rejection detected',
228
+ data: { error: error.message, stack: error.stack }
229
+ })
230
+ })
231
+
232
+ process.on('uncaughtException', error => {
233
+ Logger.log({
234
+ level: 'error',
235
+ message: 'Uncaught exception detected',
236
+ data: { error: error.message, stack: error.stack }
237
+ })
238
+ })
@@ -0,0 +1,9 @@
1
+ import { Logic } from './Logic.js'
2
+
3
+ export const Controllers = {
4
+ async getStats (ctx) {
5
+ const { traceId } = ctx.state
6
+ const result = await Logic.getStats({ traceId })
7
+ ctx.reply(result)
8
+ }
9
+ }
@@ -0,0 +1,76 @@
1
+ import { SqliteStore } from '../../singletons/SqliteStore.js'
2
+ import {
3
+ PUBSUB_TOPICS,
4
+ PUBSUB_SUBSCRIPTIONS,
5
+ PUBSUB_MESSAGES,
6
+ LOGGING_ENTRIES,
7
+ MQTT_CLIENTS,
8
+ MQTT_MESSAGES
9
+ } from '../../db/Tables.js'
10
+ import { Logic as FirestoreLogic } from '../firestore/Logic.js'
11
+
12
+ export const Logic = {
13
+ async getStats (params) {
14
+ const { traceId } = params
15
+ try {
16
+ const topics = SqliteStore.list(PUBSUB_TOPICS)
17
+ const subscriptions = SqliteStore.list(PUBSUB_SUBSCRIPTIONS)
18
+ const messages = SqliteStore.list(PUBSUB_MESSAGES)
19
+ const logEntries = SqliteStore.list(LOGGING_ENTRIES)
20
+ const loggingByLevel = {}
21
+ logEntries.data.forEach(entry => {
22
+ const level = entry.severity || 'UNKNOWN'
23
+ loggingByLevel[level] = (loggingByLevel[level] || 0) + 1
24
+ })
25
+ const mqttClients = SqliteStore.list(MQTT_CLIENTS)
26
+ const mqttMessages = SqliteStore.list(MQTT_MESSAGES)
27
+ let firestoreStats = { collections: 0, documents: 0 }
28
+ try {
29
+ const collectionsResult = await FirestoreLogic.listCollections({ traceId })
30
+ if (collectionsResult.status !== 'error' && collectionsResult.collections) {
31
+ const collectionDocCounts = await Promise.all(
32
+ collectionsResult.collections.map(async (collectionPath) => {
33
+ const docsResult = await FirestoreLogic.listDocuments({ collectionPath, page: { limit: 1000 }, traceId })
34
+ return docsResult.status !== 'error' ? docsResult.total : 0
35
+ })
36
+ )
37
+ const totalDocuments = collectionDocCounts.reduce((sum, count) => sum + count, 0)
38
+ firestoreStats = {
39
+ collections: collectionsResult.total,
40
+ documents: totalDocuments
41
+ }
42
+ }
43
+ } catch (firestoreError) {
44
+ firestoreStats = { collections: 0, documents: 0 }
45
+ }
46
+
47
+ const stats = {
48
+ pubsub: {
49
+ topics: topics.total,
50
+ subscriptions: subscriptions.total,
51
+ messages: messages.total
52
+ },
53
+ logging: {
54
+ entries: logEntries.total,
55
+ byLevel: loggingByLevel
56
+ },
57
+ mqtt: {
58
+ clients: mqttClients.total,
59
+ messages: mqttMessages.total
60
+ },
61
+ firestore: firestoreStats
62
+ }
63
+ return {
64
+ status: 'ok',
65
+ data: stats,
66
+ traceId
67
+ }
68
+ } catch (error) {
69
+ return {
70
+ status: 'error',
71
+ message: error.message,
72
+ traceId
73
+ }
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,11 @@
1
+ import KoaRouter from 'koa-router'
2
+ import { Controllers } from './Controllers.js'
3
+
4
+ const Router = new KoaRouter()
5
+ const v1 = new KoaRouter({ prefix: '/v1/dashboard' })
6
+
7
+ v1.post('/stats', Controllers.getStats)
8
+
9
+ Router.use(v1.routes())
10
+
11
+ export { Router }
@@ -0,0 +1,47 @@
1
+ import { Joi } from '@gokiteam/koa'
2
+
3
+ export const Schemas = {
4
+ stats: {
5
+ request: {
6
+ body: {}
7
+ },
8
+ responses: {
9
+ success: {
10
+ stats: Joi.object({
11
+ topics: Joi.number().integer().min(0).required(),
12
+ subscriptions: Joi.number().integer().min(0).required(),
13
+ messages: Joi.number().integer().min(0).required(),
14
+ logEntries: Joi.number().integer().min(0).required(),
15
+ mqttClients: Joi.number().integer().min(0).required(),
16
+ mqttMessages: Joi.number().integer().min(0).required()
17
+ }).required()
18
+ }
19
+ }
20
+ },
21
+ recentActivity: {
22
+ request: {
23
+ body: {
24
+ limit: Joi.number().integer().min(1).max(100).default(20)
25
+ }
26
+ },
27
+ responses: {
28
+ success: {
29
+ activities: Joi.array().items(
30
+ Joi.object({
31
+ type: Joi.string().valid(
32
+ 'pubsub.message',
33
+ 'pubsub.topic.created',
34
+ 'pubsub.subscription.created',
35
+ 'mqtt.publish',
36
+ 'mqtt.connect',
37
+ 'mqtt.disconnect',
38
+ 'logging.entry'
39
+ ).required(),
40
+ data: Joi.object().required(),
41
+ timestamp: Joi.string().isoDate().required()
42
+ })
43
+ ).required()
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,26 @@
1
+ import { Logic } from './Logic.js'
2
+
3
+ export const Controllers = {
4
+ async export (ctx) {
5
+ const { traceId } = ctx.state
6
+ const result = await Logic.export({ traceId })
7
+ ctx.reply(result)
8
+ },
9
+ async import (ctx) {
10
+ const { traceId } = ctx.state
11
+ const { data } = ctx.request.body
12
+ const result = await Logic.import({ data, traceId })
13
+ ctx.reply(result)
14
+ },
15
+ async clear (ctx) {
16
+ const { traceId } = ctx.state
17
+ const result = await Logic.clear({ traceId })
18
+ ctx.reply(result)
19
+ },
20
+ async clearServices (ctx) {
21
+ const { traceId } = ctx.state
22
+ const { services, keepSystemData } = ctx.request.body
23
+ const result = await Logic.clearServices({ services, keepSystemData, traceId })
24
+ ctx.reply(result)
25
+ }
26
+ }