@gokiteam/goki-dev 0.2.0 → 0.2.1

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.
@@ -0,0 +1,38 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install Java JRE 21 for Firebase Emulator + build tools for better-sqlite3 + Docker CLI + PostgreSQL client
6
+ RUN apk add --no-cache openjdk21-jre bash python3 make g++ docker-cli postgresql-client
7
+
8
+ # Install Firebase CLI globally for Firestore emulator
9
+ RUN npm install -g firebase-tools
10
+
11
+ # Copy package files and npm config
12
+ COPY package*.json ./
13
+
14
+ # Build arg for npm authentication
15
+ ARG NPM_TOKEN
16
+ ENV NPM_TOKEN=${NPM_TOKEN}
17
+ COPY .npmrc ./
18
+
19
+ # Install dependencies (including optional native deps like better-sqlite3)
20
+ RUN npm install --omit=dev --include=optional
21
+
22
+ # Copy source code
23
+ COPY src/ ./src/
24
+ COPY config.development ./
25
+ COPY config.test ./
26
+
27
+ # Copy Firebase configuration
28
+ COPY firebase.json ./
29
+ COPY firestore.rules ./
30
+
31
+ # Create data and logs directories
32
+ RUN mkdir -p /app/data /app/logs /app/data/firestore
33
+
34
+ # Expose ports
35
+ EXPOSE 9000 8085 8087 8883 8080
36
+
37
+ # Start server directly (env vars set via docker-compose)
38
+ CMD ["node", "src/Server.js"]
package/Dockerfile.dev ADDED
@@ -0,0 +1,38 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Install Java JRE 21 for Firebase Emulator + build tools for better-sqlite3 + PostgreSQL client
6
+ RUN apk add --no-cache openjdk21-jre bash python3 make g++ postgresql-client
7
+
8
+ # Install Firebase CLI globally for Firestore emulator
9
+ RUN npm install -g firebase-tools
10
+
11
+ # Copy package files and npm config
12
+ COPY package*.json ./
13
+
14
+ # Build arg for npm authentication
15
+ ARG NPM_TOKEN
16
+ ENV NPM_TOKEN=${NPM_TOKEN}
17
+ COPY .npmrc ./
18
+
19
+ # Install ALL dependencies (including dev for watch mode)
20
+ RUN npm install
21
+
22
+ # Install nodemon for reliable file watching in Docker (fs.watch doesn't work with bind mounts)
23
+ RUN npm install -g nodemon
24
+
25
+ # Copy config files
26
+ COPY config.development ./
27
+ COPY config.test ./
28
+ COPY firebase.json ./
29
+ COPY firestore.rules ./
30
+
31
+ # Create data and logs directories
32
+ RUN mkdir -p /app/data /app/logs /app/data/firestore
33
+
34
+ # Expose ports
35
+ EXPOSE 9000 8085 8087 8883 8080
36
+
37
+ # Start server with watch mode using nodemon (polling for Docker bind mount compatibility)
38
+ CMD ["nodemon", "--watch", "src", "--ext", "js,json", "--legacy-watch", "--polling-interval", "1000", "src/Server.js"]
@@ -0,0 +1,37 @@
1
+ # Build stage
2
+ FROM node:20-alpine AS build
3
+
4
+ WORKDIR /app
5
+
6
+ # Copy UI package files
7
+ COPY ui/package*.json ./ui/
8
+
9
+ # Install UI dependencies
10
+ WORKDIR /app/ui
11
+ RUN npm ci
12
+
13
+ # Copy UI source files
14
+ COPY ui/ ./
15
+
16
+ # Build the React app
17
+ # API URL will be injected at runtime via environment variable
18
+ RUN npm run build
19
+
20
+ # Production stage
21
+ FROM nginx:1.25-alpine
22
+
23
+ # Copy nginx configuration
24
+ COPY nginx.conf /etc/nginx/conf.d/default.conf
25
+
26
+ # Copy built files from build stage
27
+ COPY --from=build /app/ui/build /usr/share/nginx/html
28
+
29
+ # Add healthcheck
30
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
31
+ CMD wget --quiet --tries=1 --spider http://127.0.0.1/health || exit 1
32
+
33
+ # Expose port
34
+ EXPOSE 80
35
+
36
+ # Start nginx
37
+ CMD ["nginx", "-g", "daemon off;"]
@@ -314,6 +314,53 @@ export declare class SchedulerClient {
314
314
  traceId?: string;
315
315
  }): Promise<Types.SchedulerTickResponse>;
316
316
  }
317
+ export declare class HttpTrafficClient {
318
+ private client;
319
+ constructor(client: AxiosInstance);
320
+ private call;
321
+ list(params?: {
322
+ filter?: Types.HttpTrafficFilter;
323
+ limit?: number;
324
+ offset?: number;
325
+ traceId?: string;
326
+ }): Promise<{
327
+ entries: Types.HttpTrafficEntry[];
328
+ total: number;
329
+ limit: number;
330
+ offset: number;
331
+ } & {
332
+ traceId: string;
333
+ }>;
334
+ getDetails(params: {
335
+ id: string;
336
+ traceId?: string;
337
+ }): Promise<{
338
+ entry: Types.HttpTrafficEntry;
339
+ } & {
340
+ traceId: string;
341
+ }>;
342
+ clear(params?: {
343
+ traceId?: string;
344
+ }): Promise<{
345
+ message: string;
346
+ } & {
347
+ traceId: string;
348
+ }>;
349
+ waitFor(params: {
350
+ filter: Types.HttpTrafficWaitFilter;
351
+ timeout?: number;
352
+ traceId?: string;
353
+ }): Promise<{
354
+ entry: Types.HttpTrafficEntry;
355
+ foundAt: number;
356
+ traceId: string;
357
+ }>;
358
+ stats(params?: {
359
+ traceId?: string;
360
+ }): Promise<Types.HttpTrafficStats & {
361
+ traceId: string;
362
+ }>;
363
+ }
317
364
  export declare class DevToolsClient {
318
365
  private client;
319
366
  private baseUrl;
@@ -326,6 +373,7 @@ export declare class DevToolsClient {
326
373
  docker: DockerClient;
327
374
  platform: PlatformClient;
328
375
  scheduler: SchedulerClient;
376
+ httpTraffic: HttpTrafficClient;
329
377
  constructor(config?: DevToolsClientConfig);
330
378
  generateTraceId(prefix?: string): string;
331
379
  }
@@ -22,7 +22,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
22
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
- exports.DevToolsClient = exports.SchedulerClient = exports.PlatformClient = exports.DockerClient = exports.MqttClient = exports.FirestoreClient = exports.RedisClient = exports.PostgresClient = exports.LoggingClient = exports.PubSubClient = void 0;
25
+ exports.DevToolsClient = exports.HttpTrafficClient = exports.SchedulerClient = exports.PlatformClient = exports.DockerClient = exports.MqttClient = exports.FirestoreClient = exports.RedisClient = exports.PostgresClient = exports.LoggingClient = exports.PubSubClient = void 0;
26
26
  const axios_1 = __importDefault(require("axios"));
27
27
  // ============================================================================
28
28
  // PubSub Client
@@ -473,6 +473,55 @@ class SchedulerClient {
473
473
  }
474
474
  exports.SchedulerClient = SchedulerClient;
475
475
  // ============================================================================
476
+ // HTTP Traffic Client
477
+ // ============================================================================
478
+ class HttpTrafficClient {
479
+ constructor(client) {
480
+ this.client = client;
481
+ }
482
+ async call(endpoint, data = {}) {
483
+ const response = await this.client.post(endpoint, data);
484
+ return response.data;
485
+ }
486
+ async list(params = {}) {
487
+ const result = await this.call('/v1/http-traffic/list', {
488
+ filter: params.filter,
489
+ limit: params.limit,
490
+ offset: params.offset,
491
+ traceId: params.traceId
492
+ });
493
+ return result.data;
494
+ }
495
+ async getDetails(params) {
496
+ const result = await this.call('/v1/http-traffic/details', {
497
+ id: params.id,
498
+ traceId: params.traceId
499
+ });
500
+ return result.data;
501
+ }
502
+ async clear(params = {}) {
503
+ const result = await this.call('/v1/http-traffic/clear', {
504
+ traceId: params.traceId
505
+ });
506
+ return result.data;
507
+ }
508
+ async waitFor(params) {
509
+ const result = await this.call('/v1/http-traffic/wait-for', {
510
+ filter: params.filter,
511
+ timeout: params.timeout,
512
+ traceId: params.traceId
513
+ });
514
+ return result.data;
515
+ }
516
+ async stats(params = {}) {
517
+ const result = await this.call('/v1/http-traffic/stats', {
518
+ traceId: params.traceId
519
+ });
520
+ return result.data;
521
+ }
522
+ }
523
+ exports.HttpTrafficClient = HttpTrafficClient;
524
+ // ============================================================================
476
525
  // Main DevTools Client
477
526
  // ============================================================================
478
527
  class DevToolsClient {
@@ -494,6 +543,7 @@ class DevToolsClient {
494
543
  this.docker = new DockerClient(this.client);
495
544
  this.platform = new PlatformClient(this.client);
496
545
  this.scheduler = new SchedulerClient(this.client);
546
+ this.httpTraffic = new HttpTrafficClient(this.client);
497
547
  }
498
548
  // ============================================================================
499
549
  // Utility Methods
@@ -278,3 +278,62 @@ export interface DockerContainer {
278
278
  canRestart: boolean;
279
279
  };
280
280
  }
281
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
282
+ export interface HttpTrafficEntry {
283
+ _id: string;
284
+ method: string;
285
+ targetUrl: string;
286
+ targetHost: string | null;
287
+ targetPath: string | null;
288
+ queryParams: string | null;
289
+ requestHeaders: string | null;
290
+ requestBody: string | null;
291
+ requestCookies: string | null;
292
+ contentType: string | null;
293
+ statusCode: number | null;
294
+ responseHeaders: string | null;
295
+ responseBody: string | null;
296
+ responseContentType: string | null;
297
+ responseTimeMs: number | null;
298
+ startedAt: string;
299
+ completedAt: string | null;
300
+ sourceService: string | null;
301
+ error: string | null;
302
+ traceId: string | null;
303
+ createdAt: string;
304
+ }
305
+ export interface HttpTrafficFilter {
306
+ method?: HttpMethod;
307
+ targetHost?: string;
308
+ sourceService?: string;
309
+ statusCode?: number;
310
+ traceId?: string;
311
+ since?: string;
312
+ until?: string;
313
+ }
314
+ export interface HttpTrafficWaitFilter {
315
+ method?: HttpMethod;
316
+ targetHost?: string;
317
+ sourceService?: string;
318
+ statusCode?: number;
319
+ traceId?: string;
320
+ pathContains?: string;
321
+ }
322
+ export interface HttpTrafficStats {
323
+ total: number;
324
+ errorCount: number;
325
+ errorRate: string;
326
+ avgResponseTimeMs: number;
327
+ byMethod: {
328
+ method: string;
329
+ count: number;
330
+ }[];
331
+ byHost: {
332
+ source_service: string | null;
333
+ count: number;
334
+ }[];
335
+ byStatus: {
336
+ status_code: number | null;
337
+ count: number;
338
+ }[];
339
+ }
@@ -0,0 +1,81 @@
1
+ # App services (dev-tools backend + UI)
2
+ # Requires shared services running first: docker compose -f docker-compose.services.yml up -d
3
+ # Then start: docker compose -f docker-compose.dev.yml up -d
4
+
5
+ services:
6
+ backend:
7
+ image: goki-dev-tools-backend
8
+ build:
9
+ context: .
10
+ dockerfile: Dockerfile.dev
11
+ args:
12
+ NPM_TOKEN: ${NPM_TOKEN}
13
+ container_name: goki-dev-tools-backend
14
+ ports:
15
+ - "9000:9000" # Backend API
16
+ - "8087:8087" # Cloud Logging Emulator
17
+ - "8883:8883" # MQTT Broker
18
+ environment:
19
+ - NODE_ENV=development
20
+ - WEB_UI_PORT=9000
21
+ # Shared services (use container names for cross-compose networking)
22
+ - POSTGRES_HOST=goki-postgres
23
+ - POSTGRES_PORT=5432
24
+ - POSTGRES_USER=postgres
25
+ - POSTGRES_PASSWORD=postgres
26
+ - POSTGRES_DATABASE=device_native
27
+ - REDIS_HOST=goki-redis
28
+ - REDIS_PORT=6379
29
+ - REDIS_LOGS_HOST=goki-redis-logs
30
+ - REDIS_LOGS_PORT=6380
31
+ - PUBSUB_EMULATOR_HOST=goki-pubsub-emulator:8085
32
+ - FIRESTORE_EMULATOR_HOST=goki-firestore-emulator:8080
33
+ - FIRESTORE_PROJECT_ID=tipi-development
34
+ - FIRESTORE_PROJECT_IDS=tipi-development,goki-dev-local
35
+ # App-specific config
36
+ - LOGGING_PORT=8087
37
+ - MQTT_PORT=8883
38
+ - DATA_DIR=/app/data
39
+ - AUTO_FLUSH_INTERVAL_MS=5000
40
+ - LOG_LEVEL=info
41
+ - UI_DEV_SERVER=http://goki-dev-tools-frontend:9001
42
+ - CHOKIDAR_USEPOLLING=true
43
+ - HOST_PROJECT_DIR=${HOST_PROJECT_DIR:-${PWD}}
44
+ volumes:
45
+ - ./src:/app/src:ro
46
+ - ./docs:/app/docs:ro
47
+ - ./data:/app/data
48
+ - ./logs:/app/logs
49
+ - /var/run/docker.sock:/var/run/docker.sock:ro
50
+ depends_on:
51
+ - frontend
52
+ networks:
53
+ - goki-network
54
+ restart: unless-stopped
55
+
56
+ frontend:
57
+ image: goki-dev-tools-frontend
58
+ build:
59
+ context: ./ui
60
+ dockerfile: Dockerfile.dev
61
+ container_name: goki-dev-tools-frontend
62
+ ports:
63
+ - "9001:9001" # React dev server
64
+ environment:
65
+ - PORT=9001
66
+ - CHOKIDAR_USEPOLLING=true
67
+ - WATCHPACK_POLLING=true
68
+ - WDS_SOCKET_PORT=9001
69
+ volumes:
70
+ - ./ui/src:/app/src
71
+ - ./ui/public:/app/public
72
+ - ./ui/tailwind.config.js:/app/tailwind.config.js:ro
73
+ - ./ui/postcss.config.js:/app/postcss.config.js:ro
74
+ networks:
75
+ - goki-network
76
+ restart: unless-stopped
77
+
78
+
79
+ networks:
80
+ goki-network:
81
+ external: true
@@ -0,0 +1,186 @@
1
+ # Shared infrastructure services (PostgreSQL, Redis, Emulators)
2
+ # Start these first: docker compose -f docker-compose.services.yml up -d
3
+ # Then start app services: docker compose -f docker-compose.dev.yml up -d
4
+
5
+ services:
6
+ postgres:
7
+ image: postgres:15-alpine
8
+ container_name: goki-postgres
9
+ environment:
10
+ POSTGRES_USER: postgres
11
+ POSTGRES_PASSWORD: postgres
12
+ ports:
13
+ - "5432:5432"
14
+ volumes:
15
+ - postgres-data:/var/lib/postgresql/data
16
+ networks:
17
+ - goki-network
18
+ restart: unless-stopped
19
+ labels:
20
+ - "goki.service=postgres"
21
+ healthcheck:
22
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
23
+ interval: 5s
24
+ timeout: 5s
25
+ retries: 5
26
+ profiles:
27
+ - postgres
28
+
29
+ redis:
30
+ image: redis:7-alpine
31
+ container_name: goki-redis
32
+ ports:
33
+ - "6379:6379"
34
+ volumes:
35
+ - redis-data:/data
36
+ networks:
37
+ - goki-network
38
+ restart: unless-stopped
39
+ labels:
40
+ - "goki.service=redis"
41
+ healthcheck:
42
+ test: ["CMD", "redis-cli", "ping"]
43
+ interval: 5s
44
+ timeout: 5s
45
+ retries: 5
46
+
47
+ redis-logs:
48
+ image: redis:7-alpine
49
+ container_name: goki-redis-logs
50
+ command: redis-server --port 6380 --maxmemory 256mb --maxmemory-policy allkeys-lru
51
+ ports:
52
+ - "6380:6380"
53
+ volumes:
54
+ - redis-logs-data:/data
55
+ networks:
56
+ - goki-network
57
+ restart: unless-stopped
58
+ labels:
59
+ - "goki.service=redis-logs"
60
+ healthcheck:
61
+ test: ["CMD", "redis-cli", "-p", "6380", "ping"]
62
+ interval: 5s
63
+ timeout: 5s
64
+ retries: 5
65
+ profiles:
66
+ - redis-logs
67
+
68
+ pubsub-emulator:
69
+ image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
70
+ container_name: goki-pubsub-emulator
71
+ command: gcloud beta emulators pubsub start --host-port=0.0.0.0:8085 --project=tipi-development
72
+ ports:
73
+ - "8085:8085"
74
+ networks:
75
+ - goki-network
76
+ restart: unless-stopped
77
+ labels:
78
+ - "goki.service=pubsub-emulator"
79
+ healthcheck:
80
+ test: ["CMD-SHELL", "curl -sf http://localhost:8085/v1/projects/tipi-development/topics || exit 1"]
81
+ interval: 5s
82
+ timeout: 5s
83
+ retries: 10
84
+ start_period: 10s
85
+
86
+ firestore-emulator:
87
+ image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
88
+ container_name: goki-firestore-emulator
89
+ entrypoint: /bin/bash
90
+ command:
91
+ - -c
92
+ - |
93
+ IMPORT_FILE=$$(find /data/firestore-export -name '*.overall_export_metadata' 2>/dev/null | head -1)
94
+ if [ -n "$$IMPORT_FILE" ]; then
95
+ echo "Importing Firestore data from $$IMPORT_FILE"
96
+ exec gcloud emulators firestore start --host-port=0.0.0.0:8080 --project=goki-dev-local --export-on-exit=/data/firestore-export --import-data="$$IMPORT_FILE"
97
+ else
98
+ echo "No previous export found, starting fresh"
99
+ exec gcloud emulators firestore start --host-port=0.0.0.0:8080 --project=goki-dev-local --export-on-exit=/data/firestore-export
100
+ fi
101
+ ports:
102
+ - "8080:8080"
103
+ volumes:
104
+ - firestore-data:/data
105
+ networks:
106
+ - goki-network
107
+ restart: unless-stopped
108
+ labels:
109
+ - "goki.service=firestore-emulator"
110
+ healthcheck:
111
+ test: ["CMD-SHELL", "curl -sf http://localhost:8080/ || exit 1"]
112
+ interval: 5s
113
+ timeout: 5s
114
+ retries: 10
115
+ start_period: 10s
116
+ profiles:
117
+ - firestore
118
+
119
+ elasticsearch:
120
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
121
+ container_name: goki-elasticsearch
122
+ environment:
123
+ - discovery.type=single-node
124
+ - xpack.security.enabled=false
125
+ - ES_JAVA_OPTS=-Xms1g -Xmx1g
126
+ ports:
127
+ - "9200:9200"
128
+ volumes:
129
+ - elasticsearch-data:/usr/share/elasticsearch/data
130
+ networks:
131
+ - goki-network
132
+ restart: unless-stopped
133
+ labels:
134
+ - "goki.service=elasticsearch"
135
+ deploy:
136
+ resources:
137
+ limits:
138
+ memory: 2g
139
+ healthcheck:
140
+ test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
141
+ interval: 10s
142
+ timeout: 5s
143
+ retries: 10
144
+ start_period: 30s
145
+ profiles:
146
+ - elasticsearch
147
+
148
+ kibana:
149
+ image: docker.elastic.co/kibana/kibana:8.12.0
150
+ container_name: goki-kibana
151
+ environment:
152
+ - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
153
+ ports:
154
+ - "5601:5601"
155
+ depends_on:
156
+ elasticsearch:
157
+ condition: service_healthy
158
+ networks:
159
+ - goki-network
160
+ restart: unless-stopped
161
+ labels:
162
+ - "goki.service=kibana"
163
+ deploy:
164
+ resources:
165
+ limits:
166
+ memory: 1g
167
+ healthcheck:
168
+ test: ["CMD-SHELL", "curl -sf http://localhost:5601/api/status || exit 1"]
169
+ interval: 10s
170
+ timeout: 5s
171
+ retries: 10
172
+ start_period: 60s
173
+ profiles:
174
+ - kibana
175
+
176
+ networks:
177
+ goki-network:
178
+ name: goki-network
179
+ driver: bridge
180
+
181
+ volumes:
182
+ postgres-data:
183
+ redis-data:
184
+ redis-logs-data:
185
+ firestore-data:
186
+ elasticsearch-data:
@@ -0,0 +1,233 @@
1
+ services:
2
+ pubsub-emulator:
3
+ image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
4
+ container_name: goki-pubsub-emulator
5
+ command: gcloud beta emulators pubsub start --host-port=0.0.0.0:8085 --project=tipi-development
6
+ ports:
7
+ - "8085:8085"
8
+ networks:
9
+ - goki-network
10
+ restart: unless-stopped
11
+ labels:
12
+ - "goki.service=pubsub-emulator"
13
+ healthcheck:
14
+ test: ["CMD", "curl", "-f", "http://localhost:8085"]
15
+ interval: 5s
16
+ timeout: 5s
17
+ retries: 10
18
+ start_period: 10s
19
+
20
+ firestore-emulator:
21
+ image: gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
22
+ container_name: goki-firestore-emulator
23
+ entrypoint: /bin/bash
24
+ command:
25
+ - -c
26
+ - |
27
+ IMPORT_FILE=$$(find /data/firestore-export -name '*.overall_export_metadata' 2>/dev/null | head -1)
28
+ if [ -n "$$IMPORT_FILE" ]; then
29
+ echo "Importing Firestore data from $$IMPORT_FILE"
30
+ exec gcloud emulators firestore start --host-port=0.0.0.0:8081 --project=goki-dev-local --export-on-exit=/data/firestore-export --import-data="$$IMPORT_FILE"
31
+ else
32
+ echo "No previous export found, starting fresh"
33
+ exec gcloud emulators firestore start --host-port=0.0.0.0:8081 --project=goki-dev-local --export-on-exit=/data/firestore-export
34
+ fi
35
+ ports:
36
+ - "8081:8081"
37
+ volumes:
38
+ - firestore-data:/data
39
+ networks:
40
+ - goki-network
41
+ restart: unless-stopped
42
+ labels:
43
+ - "goki.service=firestore-emulator"
44
+ healthcheck:
45
+ test: ["CMD-SHELL", "curl -sf http://localhost:8081 || exit 1"]
46
+ interval: 10s
47
+ timeout: 10s
48
+ retries: 12
49
+ start_period: 60s
50
+ profiles:
51
+ - firestore
52
+
53
+ dev-tools-backend:
54
+ build:
55
+ context: .
56
+ dockerfile: Dockerfile.backend
57
+ args:
58
+ NPM_TOKEN: ${NPM_TOKEN}
59
+ container_name: goki-dev-tools-backend
60
+ ports:
61
+ - "9000:9000" # Backend API
62
+ - "8086:8086" # AWS IoT Core HTTPS API
63
+ - "8087:8087" # Cloud Logging Emulator
64
+ - "8883:8883" # MQTT Broker
65
+ labels:
66
+ - "goki.service=dev-tools-backend"
67
+ environment:
68
+ - NODE_ENV=production
69
+ - WEB_UI_PORT=9000
70
+ - PUBSUB_EMULATOR_HOST=pubsub-emulator:8085
71
+ - PUBSUB_PROJECT_ID=tipi-development
72
+ - SHADOW_POLL_INTERVAL_MS=200
73
+ - SHADOW_SUBSCRIPTION_CHECK_INTERVAL_MS=5000
74
+ - LOGGING_PORT=8087
75
+ - MQTT_PORT=8883
76
+ - AWS_IOT_PORT=8086
77
+ - FIRESTORE_EMULATOR_HOST=goki-firestore-emulator:8080
78
+ - FIRESTORE_PORT=8080
79
+ - FIRESTORE_PROJECT_ID=tipi-development
80
+ - DATA_DIR=/app/data
81
+ - AUTO_FLUSH_INTERVAL_MS=5000
82
+ - REDIS_HOST=redis
83
+ - REDIS_PORT=6379
84
+ - POSTGRES_HOST=postgres
85
+ - POSTGRES_PORT=5432
86
+ - POSTGRES_USER=postgres
87
+ - POSTGRES_PASSWORD=postgres
88
+ - LOG_LEVEL=info
89
+ # App Gateway config (use container names)
90
+ - APP_GATEWAY_PROPERTY_ID=550e8400-e29b-41d4-a716-446655440000
91
+ - APP_GATEWAY_DEVICE_NATIVE_URL=http://device-native-app:3000
92
+ - APP_GATEWAY_DEVICE_SIMULATOR_URL=http://device-simulator-app:3000
93
+ - APP_GATEWAY_SCAN_INTERVAL_SECONDS=30
94
+ - APP_GATEWAY_AUTO_START=true
95
+ volumes:
96
+ - ./data:/app/data
97
+ - ./logs:/app/logs
98
+ - /var/run/docker.sock:/var/run/docker.sock
99
+ depends_on:
100
+ pubsub-emulator:
101
+ condition: service_healthy
102
+ redis:
103
+ condition: service_started
104
+ # Note: firestore and postgres are optional (have profiles)
105
+ # App should gracefully handle missing connections
106
+ networks:
107
+ - goki-network
108
+ restart: unless-stopped
109
+
110
+ dev-tools-frontend:
111
+ build:
112
+ context: .
113
+ dockerfile: Dockerfile.frontend
114
+ container_name: goki-dev-tools-frontend
115
+ ports:
116
+ - "9001:80" # Frontend UI
117
+ labels:
118
+ - "goki.service=dev-tools-frontend"
119
+ depends_on:
120
+ - dev-tools-backend
121
+ networks:
122
+ - goki-network
123
+ restart: unless-stopped
124
+ healthcheck:
125
+ test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://127.0.0.1/health || exit 1"]
126
+ interval: 30s
127
+ timeout: 3s
128
+ start_period: 10s
129
+ retries: 3
130
+
131
+ redis:
132
+ image: redis:7-alpine
133
+ container_name: goki-redis
134
+ ports:
135
+ - "6379:6379"
136
+ volumes:
137
+ - redis-data:/data
138
+ networks:
139
+ - goki-network
140
+ restart: unless-stopped
141
+ labels:
142
+ - "goki.service=redis"
143
+
144
+ postgres:
145
+ image: postgres:15-alpine
146
+ container_name: goki-postgres
147
+ environment:
148
+ POSTGRES_USER: postgres
149
+ POSTGRES_PASSWORD: postgres
150
+ ports:
151
+ - "5432:5432"
152
+ volumes:
153
+ - postgres-data:/var/lib/postgresql/data
154
+ networks:
155
+ - goki-network
156
+ restart: unless-stopped
157
+ labels:
158
+ - "goki.service=postgres"
159
+ healthcheck:
160
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
161
+ interval: 5s
162
+ timeout: 5s
163
+ retries: 5
164
+ profiles:
165
+ - postgres
166
+
167
+ elasticsearch:
168
+ image: docker.elastic.co/elasticsearch/elasticsearch:8.12.0
169
+ container_name: goki-elasticsearch
170
+ environment:
171
+ - discovery.type=single-node
172
+ - xpack.security.enabled=false
173
+ - ES_JAVA_OPTS=-Xms1g -Xmx1g
174
+ ports:
175
+ - "9200:9200"
176
+ volumes:
177
+ - elasticsearch-data:/usr/share/elasticsearch/data
178
+ networks:
179
+ - goki-network
180
+ restart: unless-stopped
181
+ labels:
182
+ - "goki.service=elasticsearch"
183
+ deploy:
184
+ resources:
185
+ limits:
186
+ memory: 2g
187
+ healthcheck:
188
+ test: ["CMD-SHELL", "curl -sf http://localhost:9200/_cluster/health || exit 1"]
189
+ interval: 10s
190
+ timeout: 5s
191
+ retries: 10
192
+ start_period: 30s
193
+ profiles:
194
+ - elasticsearch
195
+
196
+ kibana:
197
+ image: docker.elastic.co/kibana/kibana:8.12.0
198
+ container_name: goki-kibana
199
+ environment:
200
+ - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
201
+ ports:
202
+ - "5601:5601"
203
+ depends_on:
204
+ elasticsearch:
205
+ condition: service_healthy
206
+ networks:
207
+ - goki-network
208
+ restart: unless-stopped
209
+ labels:
210
+ - "goki.service=kibana"
211
+ deploy:
212
+ resources:
213
+ limits:
214
+ memory: 1g
215
+ healthcheck:
216
+ test: ["CMD-SHELL", "curl -sf http://localhost:5601/api/status || exit 1"]
217
+ interval: 10s
218
+ timeout: 5s
219
+ retries: 10
220
+ start_period: 60s
221
+ profiles:
222
+ - kibana
223
+
224
+ networks:
225
+ goki-network:
226
+ name: goki-network
227
+ driver: bridge
228
+
229
+ volumes:
230
+ redis-data:
231
+ postgres-data:
232
+ firestore-data:
233
+ elasticsearch-data:
package/nginx.conf ADDED
@@ -0,0 +1,41 @@
1
+ server {
2
+ listen 80;
3
+ server_name localhost;
4
+ root /usr/share/nginx/html;
5
+ index index.html;
6
+
7
+ # Gzip compression
8
+ gzip on;
9
+ gzip_vary on;
10
+ gzip_min_length 1024;
11
+ gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
12
+
13
+ # Security headers
14
+ add_header X-Frame-Options "SAMEORIGIN" always;
15
+ add_header X-Content-Type-Options "nosniff" always;
16
+ add_header X-XSS-Protection "1; mode=block" always;
17
+
18
+ # Serve static files
19
+ location / {
20
+ try_files $uri $uri/ /index.html;
21
+ }
22
+
23
+ # Cache static assets
24
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
25
+ expires 1y;
26
+ add_header Cache-Control "public, immutable";
27
+ }
28
+
29
+ # Don't cache index.html
30
+ location = /index.html {
31
+ add_header Cache-Control "no-store, no-cache, must-revalidate";
32
+ expires 0;
33
+ }
34
+
35
+ # Health check endpoint
36
+ location /health {
37
+ access_log off;
38
+ return 200 "healthy\n";
39
+ add_header Content-Type text/plain;
40
+ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gokiteam/goki-dev",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Unified local development platform for Goki services",
5
5
  "type": "module",
6
6
  "main": "./client/dist/index.js",
@@ -29,6 +29,14 @@
29
29
  "cli/",
30
30
  "src/",
31
31
  "client/dist/",
32
+ "ui/build/",
33
+ "docker-compose.yml",
34
+ "docker-compose.services.yml",
35
+ "docker-compose.dev.yml",
36
+ "Dockerfile.backend",
37
+ "Dockerfile.frontend",
38
+ "Dockerfile.dev",
39
+ "nginx.conf",
32
40
  "config.development",
33
41
  "config.test",
34
42
  "guidelines/",
@@ -42,7 +50,10 @@
42
50
  "ui:start": "cd ui && npm start",
43
51
  "ui:build": "cd ui && npm run build",
44
52
  "build:client": "cd client && npx tsc && echo '{\"type\":\"commonjs\"}' > dist/package.json",
45
- "prepublishOnly": "npm run build:client",
53
+ "prepublishOnly": "npm run ui:build && npm run build:client",
54
+ "release": "npm version patch && npm run build:client && npm publish",
55
+ "release:minor": "npm version minor && npm run build:client && npm publish",
56
+ "release:major": "npm version major && npm run build:client && npm publish",
46
57
  "test": "dotenv -e config.test mocha 'tests/**/*.test.js'",
47
58
  "test:api": "dotenv -e config.test mocha 'tests/api/**/*.test.js'",
48
59
  "test:emulation": "dotenv -e config.test mocha 'tests/emulation/**/*.test.js'",
@@ -15,6 +15,13 @@ export const Controllers = {
15
15
  ctx.reply(result)
16
16
  },
17
17
 
18
+ async waitFor (ctx) {
19
+ const { traceId } = ctx.state
20
+ const { filter, timeout } = ctx.request.body
21
+ const result = await Logic.waitForTraffic({ filter, timeout, traceId })
22
+ ctx.reply(result)
23
+ },
24
+
18
25
  async clear (ctx) {
19
26
  const { traceId } = ctx.state
20
27
  const result = Logic.clear({ traceId })
@@ -1,5 +1,7 @@
1
1
  import { HttpProxy } from '../../singletons/HttpProxy.js'
2
2
 
3
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
4
+
3
5
  export const Logic = {
4
6
  list (params) {
5
7
  const { filter, limit = 50, offset = 0, traceId } = params
@@ -19,6 +21,32 @@ export const Logic = {
19
21
  return { entry, traceId }
20
22
  },
21
23
 
24
+ async waitForTraffic (params) {
25
+ const { filter, timeout = 5000, traceId } = params
26
+ const startTime = Date.now()
27
+ const pollInterval = 100
28
+ const { pathContains, ...equalityFilter } = filter
29
+ try {
30
+ while (Date.now() - startTime < timeout) {
31
+ const { data } = HttpProxy.listTraffic({ filter: equalityFilter, limit: 50 })
32
+ if (data && data.length > 0) {
33
+ for (const entry of data) {
34
+ if (!pathContains) {
35
+ return { entry, foundAt: Date.now() - startTime, traceId }
36
+ }
37
+ if (entry.targetPath && entry.targetPath.includes(pathContains)) {
38
+ return { entry, foundAt: Date.now() - startTime, traceId }
39
+ }
40
+ }
41
+ }
42
+ await sleep(pollInterval)
43
+ }
44
+ return { status: 'error', message: 'Timeout waiting for HTTP traffic', traceId }
45
+ } catch (error) {
46
+ return { status: 'error', message: error.message, traceId }
47
+ }
48
+ },
49
+
22
50
  clear (params) {
23
51
  const { traceId } = params
24
52
  HttpProxy.clearTraffic()
@@ -5,5 +5,6 @@ export const Router = new KoaRouter({ prefix: '/v1/http-traffic' })
5
5
 
6
6
  Router.post('/list', Controllers.list)
7
7
  Router.post('/details', Controllers.details)
8
+ Router.post('/wait-for', Controllers.waitFor)
8
9
  Router.post('/clear', Controllers.clear)
9
10
  Router.post('/stats', Controllers.stats)
@@ -7,7 +7,9 @@ export const Schemas = {
7
7
  targetHost: Joi.string(),
8
8
  sourceService: Joi.string(),
9
9
  statusCode: Joi.number().integer(),
10
- traceId: Joi.string()
10
+ traceId: Joi.string(),
11
+ since: Joi.string().isoDate(),
12
+ until: Joi.string().isoDate()
11
13
  }).optional(),
12
14
  limit: Joi.number().integer().min(1).max(500).default(50),
13
15
  offset: Joi.number().integer().min(0).default(0)
@@ -17,6 +19,18 @@ export const Schemas = {
17
19
  id: Joi.string().required()
18
20
  }),
19
21
 
22
+ waitFor: Joi.object({
23
+ filter: Joi.object({
24
+ method: Joi.string().valid('GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'),
25
+ targetHost: Joi.string(),
26
+ sourceService: Joi.string(),
27
+ statusCode: Joi.number().integer(),
28
+ traceId: Joi.string(),
29
+ pathContains: Joi.string()
30
+ }).required(),
31
+ timeout: Joi.number().integer().min(100).max(30000).default(5000)
32
+ }),
33
+
20
34
  clear: Joi.object({}),
21
35
 
22
36
  stats: Joi.object({})
@@ -5,7 +5,7 @@ export function registerHttpTrafficTools (server, apiClient) {
5
5
  'http_traffic_list',
6
6
  'List recorded HTTP traffic between microservices (captured by the dev-tools proxy)',
7
7
  {
8
- filter: z.record(z.any()).optional().describe('Optional filter criteria (method, targetHost, sourceService, statusCode, traceId)'),
8
+ filter: z.record(z.any()).optional().describe('Optional filter criteria (method, targetHost, sourceService, statusCode, traceId, since, until)'),
9
9
  limit: z.number().optional().describe('Maximum number of records to return'),
10
10
  offset: z.number().optional().describe('Number of records to skip for pagination')
11
11
  },
@@ -39,6 +39,25 @@ export function registerHttpTrafficTools (server, apiClient) {
39
39
  }
40
40
  )
41
41
 
42
+ server.tool(
43
+ 'http_traffic_wait_for',
44
+ 'Wait for an HTTP traffic entry matching the filter to appear within a timeout (polling). Useful for e2e tests.',
45
+ {
46
+ filter: z.record(z.any()).describe('Required filter criteria (method, targetHost, sourceService, statusCode, traceId, pathContains)'),
47
+ timeout: z.number().optional().describe('Timeout in ms (default: 5000, max: 30000)')
48
+ },
49
+ async ({ filter, timeout }) => {
50
+ try {
51
+ const body = { filter }
52
+ if (timeout !== undefined) body.timeout = timeout
53
+ const data = await apiClient.post('/v1/http-traffic/wait-for', body)
54
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
55
+ } catch (error) {
56
+ return { content: [{ type: 'text', text: `Failed to wait for HTTP traffic: ${error.message}` }], isError: true }
57
+ }
58
+ }
59
+ )
60
+
42
61
  server.tool(
43
62
  'http_traffic_clear',
44
63
  'Clear all recorded HTTP traffic',
@@ -174,7 +174,17 @@ class HttpProxyClass {
174
174
  offset
175
175
  }
176
176
  if (filter && Object.keys(filter).length > 0) {
177
- listOptions.filter = filter
177
+ const { since, until, ...equalityFilter } = filter
178
+ if (Object.keys(equalityFilter).length > 0) {
179
+ listOptions.filter = equalityFilter
180
+ }
181
+ const conditions = []
182
+ const params = []
183
+ if (since) { conditions.push('created_at >= ?'); params.push(since) }
184
+ if (until) { conditions.push('created_at <= ?'); params.push(until) }
185
+ if (conditions.length > 0) {
186
+ listOptions.rawWhere = { sql: conditions.join(' AND '), params }
187
+ }
178
188
  }
179
189
  return SqliteStore.list(HTTP_TRAFFIC, listOptions)
180
190
  }
@@ -147,8 +147,12 @@ class SqliteStoreClass {
147
147
  params.push(...where.params)
148
148
  }
149
149
 
150
- const countSql = `SELECT COUNT(*) as count FROM ${tableName}` +
151
- (params.length > 0 ? ` ${this.buildWhereClause(options.where || options.filter).sql}` : '')
150
+ if (options.rawWhere) {
151
+ sql += params.length > 0 ? ` AND ${options.rawWhere.sql}` : ` WHERE ${options.rawWhere.sql}`
152
+ params.push(...options.rawWhere.params)
153
+ }
154
+
155
+ const countSql = sql.replace(/^SELECT \* FROM/, 'SELECT COUNT(*) as count FROM')
152
156
  const totalResult = this.db.prepare(countSql).get(...params)
153
157
  const total = totalResult.count
154
158
 
@@ -0,0 +1,19 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+
8
+ # Install dependencies
9
+ RUN npm install
10
+
11
+ # Copy config files (source files will be mounted as volumes)
12
+ COPY tailwind.config.js ./
13
+ COPY postcss.config.js ./
14
+
15
+ # Expose React dev server port
16
+ EXPOSE 9001
17
+
18
+ # Start React dev server
19
+ CMD ["npm", "start"]