wilfredwake 1.0.9 → 1.1.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.
package/.dockerignore ADDED
@@ -0,0 +1,43 @@
1
+ # Git
2
+ .git
3
+ .gitignore
4
+ .github
5
+
6
+ # Dependencies
7
+ node_modules
8
+ npm-debug.log
9
+ yarn-error.log
10
+ package-lock.json
11
+
12
+ # Environment
13
+ .env
14
+ .env.local
15
+ .env.*.local
16
+
17
+ # IDE & Editor
18
+ .vscode
19
+ .idea
20
+ *.swp
21
+ *.swo
22
+ *~
23
+ .DS_Store
24
+
25
+ # Testing
26
+ tests
27
+ .test.js
28
+
29
+ # Documentation
30
+ README.md
31
+ QUICKSTART.md
32
+ SERVICE_STATUS_LOGIC.md
33
+ LICENSE
34
+
35
+ # Build artifacts
36
+ dist
37
+ build
38
+ out
39
+
40
+ # CI/CD
41
+ .gitlab-ci.yml
42
+ .circleci
43
+ .github/workflows
package/DOCKER.md ADDED
@@ -0,0 +1,361 @@
1
+ # Docker Guide for wilfredwake
2
+
3
+ ## Overview
4
+
5
+ This guide explains how to use Docker with wilfredwake. Three Docker configurations are provided:
6
+
7
+ - **Dockerfile** - Multi-stage build supporting production and development images
8
+ - **Dockerfile.orchestrator** - Lightweight orchestrator-only image
9
+ - **docker-compose.yml** - Orchestration for all services
10
+
11
+ ---
12
+
13
+ ## Quick Start
14
+
15
+ ### Build the Production Image
16
+
17
+ ```bash
18
+ docker build -t wilfredwake:latest .
19
+ ```
20
+
21
+ ### Build the Orchestrator Image
22
+
23
+ ```bash
24
+ docker build -f Dockerfile.orchestrator -t wilfredwake-orchestrator:latest .
25
+ ```
26
+
27
+ ### Run with Docker Compose
28
+
29
+ ```bash
30
+ # Start the orchestrator server
31
+ docker-compose up orchestrator
32
+
33
+ # Run CLI commands
34
+ docker-compose run --rm cli node bin/cli.js status
35
+
36
+ # Start development environment
37
+ docker-compose up dev
38
+ ```
39
+
40
+ ---
41
+
42
+ ## Docker Images
43
+
44
+ ### Main Dockerfile
45
+
46
+ Multi-stage Dockerfile with 3 targets:
47
+
48
+ #### **production** (default)
49
+ - Alpine-based Node.js 18
50
+ - Production dependencies only
51
+ - Optimized for size (~200MB)
52
+ - Includes health checks for orchestrator mode
53
+
54
+ **Build:**
55
+ ```bash
56
+ docker build -t wilfredwake:latest .
57
+ ```
58
+
59
+ **Run:**
60
+ ```bash
61
+ # Show help
62
+ docker run --rm wilfredwake:latest bin/cli.js --help
63
+
64
+ # Check status
65
+ docker run --rm -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest bin/cli.js status
66
+
67
+ # Wake a service
68
+ docker run --rm -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest bin/cli.js wake [service-name]
69
+
70
+ # Run orchestrator
71
+ docker run -p 3000:3000 -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest src/orchestrator/server.js
72
+ ```
73
+
74
+ #### **development**
75
+ - Alpine-based Node.js 18
76
+ - All dependencies (including devDependencies)
77
+ - Watch mode enabled (`--experimental-watch`)
78
+ - Full source code mounted via volumes
79
+
80
+ **Build:**
81
+ ```bash
82
+ docker build --target development -t wilfredwake:dev .
83
+ ```
84
+
85
+ **Run:**
86
+ ```bash
87
+ docker run -v $(pwd):/app wilfredwake:dev --experimental-watch bin/cli.js --help
88
+ ```
89
+
90
+ ### Orchestrator Dockerfile
91
+
92
+ Lightweight image for just the orchestrator server:
93
+
94
+ **Build:**
95
+ ```bash
96
+ docker build -f Dockerfile.orchestrator -t wilfredwake-orchestrator:latest .
97
+ ```
98
+
99
+ **Run:**
100
+ ```bash
101
+ docker run -p 3000:3000 \
102
+ -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml \
103
+ -e ORCHESTRATOR_PORT=3000 \
104
+ wilfredwake-orchestrator:latest
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Docker Compose
110
+
111
+ ### Services
112
+
113
+ #### **orchestrator** (default)
114
+ REST API server running on port 3000
115
+ ```bash
116
+ docker-compose up orchestrator
117
+ ```
118
+
119
+ #### **cli**
120
+ Run CLI commands
121
+ ```bash
122
+ docker-compose run --rm cli node bin/cli.js status
123
+ ```
124
+
125
+ #### **dev**
126
+ Development environment with watch mode
127
+ ```bash
128
+ docker-compose up dev
129
+ ```
130
+
131
+ ### Common Commands
132
+
133
+ ```bash
134
+ # Start orchestrator
135
+ docker-compose up orchestrator
136
+
137
+ # Run a CLI command
138
+ docker-compose run --rm cli node bin/cli.js wake my-service
139
+
140
+ # View logs
141
+ docker-compose logs -f orchestrator
142
+
143
+ # Stop all services
144
+ docker-compose down
145
+
146
+ # Clean up volumes
147
+ docker-compose down -v
148
+
149
+ # Rebuild images
150
+ docker-compose up --build orchestrator
151
+ ```
152
+
153
+ ### Configuration
154
+
155
+ Edit `docker-compose.yml` to:
156
+ - Change ports
157
+ - Add environment variables
158
+ - Mount additional volumes
159
+ - Configure networking
160
+
161
+ Example - run orchestrator on port 5000:
162
+ ```yaml
163
+ orchestrator:
164
+ ...
165
+ ports:
166
+ - "5000:3000"
167
+ environment:
168
+ - ORCHESTRATOR_PORT=5000
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Environment Variables
174
+
175
+ ### In Docker
176
+
177
+ Set via `docker run -e`:
178
+ ```bash
179
+ docker run -e NODE_ENV=production -e ORCHESTRATOR_PORT=3000 wilfredwake:latest
180
+ ```
181
+
182
+ Or in `docker-compose.yml`:
183
+ ```yaml
184
+ environment:
185
+ - NODE_ENV=production
186
+ - ORCHESTRATOR_PORT=3000
187
+ ```
188
+
189
+ ### Configuration Files
190
+
191
+ Mount your `services.yaml` and `.env`:
192
+ ```bash
193
+ docker run -v /path/to/services.yaml:/app/src/config/services.yaml \
194
+ -v /path/to/.env:/app/.env \
195
+ wilfredwake:latest
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Health Checks
201
+
202
+ Both images include health checks for orchestrator mode:
203
+
204
+ ```bash
205
+ # Check if orchestrator is healthy
206
+ docker-compose ps orchestrator
207
+
208
+ # View health status
209
+ docker inspect wilfredwake-orchestrator | grep -A 10 '"Health"'
210
+ ```
211
+
212
+ ---
213
+
214
+ ## Networking
215
+
216
+ ### Within Docker Compose
217
+ Services communicate via `wilfredwake-network`:
218
+ ```yaml
219
+ networks:
220
+ - wilfredwake-network
221
+ ```
222
+
223
+ Access orchestrator from CLI container:
224
+ ```bash
225
+ http://orchestrator:3000
226
+ ```
227
+
228
+ ### External Access
229
+ - Orchestrator exposed on `localhost:3000`
230
+ - CLI container doesn't expose ports (runs commands)
231
+
232
+ ---
233
+
234
+ ## Troubleshooting
235
+
236
+ ### Build Issues
237
+
238
+ **Missing dependencies:**
239
+ ```bash
240
+ docker build --no-cache -t wilfredwake:latest .
241
+ ```
242
+
243
+ **Verify image contents:**
244
+ ```bash
245
+ docker run -it --rm wilfredwake:latest sh
246
+ # Inside container:
247
+ # ls -la /app
248
+ # npm ls
249
+ ```
250
+
251
+ ### Runtime Issues
252
+
253
+ **Check logs:**
254
+ ```bash
255
+ docker logs wilfredwake-orchestrator
256
+ docker-compose logs -f orchestrator
257
+ ```
258
+
259
+ **Verify volume mounts:**
260
+ ```bash
261
+ docker run -it --rm -v $(pwd)/src/config/services.yaml:/app/src/config/services.yaml wilfredwake:latest ls -la src/config/
262
+ ```
263
+
264
+ **Debug networking:**
265
+ ```bash
266
+ docker network inspect wilfredwake_wilfredwake-network
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Production Considerations
272
+
273
+ 1. **Image Size**: Production image uses Alpine (~200MB)
274
+ 2. **Security**: Uses non-root Node process
275
+ 3. **Health Checks**: Built-in for orchestrator mode
276
+ 4. **Logging**: Configure via environment variables
277
+ 5. **Volumes**: Mount config files as read-only when possible
278
+
279
+ Example production deployment:
280
+ ```bash
281
+ docker run -d \
282
+ --name wilfredwake-orchestrator \
283
+ --restart=unless-stopped \
284
+ -p 3000:3000 \
285
+ -v /etc/wilfredwake/services.yaml:/app/src/config/services.yaml:ro \
286
+ -v /etc/wilfredwake/.env:/app/.env:ro \
287
+ -e NODE_ENV=production \
288
+ wilfredwake:latest \
289
+ src/orchestrator/server.js
290
+ ```
291
+
292
+ ---
293
+
294
+ ## CI/CD Integration
295
+
296
+ ### GitHub Actions Example
297
+
298
+ ```yaml
299
+ - name: Build Docker image
300
+ run: docker build -t wilfredwake:${{ github.sha }} .
301
+
302
+ - name: Run tests in container
303
+ run: docker run --rm wilfredwake:latest npm test
304
+
305
+ - name: Push to registry
306
+ run: docker push registry.example.com/wilfredwake:${{ github.sha }}
307
+ ```
308
+
309
+ ---
310
+
311
+ ## Advanced Usage
312
+
313
+ ### Custom Entrypoint
314
+
315
+ ```bash
316
+ docker run --entrypoint /bin/sh -it wilfredwake:latest
317
+ ```
318
+
319
+ ### Multi-Container Setup
320
+
321
+ ```bash
322
+ # Terminal 1: Start orchestrator
323
+ docker-compose up orchestrator
324
+
325
+ # Terminal 2: Run CLI in same network
326
+ docker-compose run --rm cli node bin/cli.js health
327
+ ```
328
+
329
+ ### Using with Kubernetes
330
+
331
+ Create Kubernetes manifests based on these Docker images. Example deployment:
332
+
333
+ ```yaml
334
+ apiVersion: v1
335
+ kind: Pod
336
+ metadata:
337
+ name: wilfredwake-orchestrator
338
+ spec:
339
+ containers:
340
+ - name: orchestrator
341
+ image: wilfredwake:latest
342
+ command: ["node", "src/orchestrator/server.js"]
343
+ ports:
344
+ - containerPort: 3000
345
+ volumeMounts:
346
+ - name: config
347
+ mountPath: /app/src/config
348
+ volumes:
349
+ - name: config
350
+ configMap:
351
+ name: wilfredwake-config
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Questions?
357
+
358
+ For more information, see:
359
+ - [README.md](README.md)
360
+ - [QUICKSTART.md](QUICKSTART.md)
361
+ - [SERVICE_STATUS_LOGIC.md](SERVICE_STATUS_LOGIC.md)
package/Dockerfile ADDED
@@ -0,0 +1,59 @@
1
+ # Multi-stage build for wilfredwake
2
+ FROM node:18-alpine AS base
3
+
4
+ WORKDIR /app
5
+
6
+ # Install dependencies stage
7
+ FROM base AS dependencies
8
+ COPY package*.json ./
9
+ RUN npm ci --only=production
10
+
11
+ # Development dependencies stage
12
+ FROM base AS dev-dependencies
13
+ COPY package*.json ./
14
+ RUN npm ci
15
+
16
+ # Production image
17
+ FROM base AS production
18
+
19
+ LABEL maintainer="Wilfred Wake"
20
+ LABEL description="CLI Tool for Multi-Developer Development Environment Wake & Status Management"
21
+
22
+ # Set environment to production
23
+ ENV NODE_ENV=production
24
+ ENV PATH="/app/node_modules/.bin:$PATH"
25
+
26
+ # Copy production dependencies
27
+ COPY --from=dependencies /app/node_modules ./node_modules
28
+
29
+ # Copy application code
30
+ COPY . .
31
+
32
+ # Make CLI executable
33
+ RUN chmod +x bin/cli.js
34
+
35
+ # Health check for orchestrator mode
36
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
37
+ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" || exit 1
38
+
39
+ # Default to CLI - can be overridden to run orchestrator server
40
+ ENTRYPOINT ["node"]
41
+ CMD ["bin/cli.js", "--help"]
42
+
43
+ # Development image
44
+ FROM base AS development
45
+
46
+ ENV NODE_ENV=development
47
+ ENV PATH="/app/node_modules/.bin:$PATH"
48
+
49
+ # Copy all dependencies (including devDependencies)
50
+ COPY --from=dev-dependencies /app/node_modules ./node_modules
51
+
52
+ # Copy application code
53
+ COPY . .
54
+
55
+ # Make CLI executable
56
+ RUN chmod +x bin/cli.js
57
+
58
+ ENTRYPOINT ["node"]
59
+ CMD ["--experimental-watch", "bin/cli.js", "--help"]
@@ -0,0 +1,29 @@
1
+ # Lightweight orchestrator-only image
2
+ FROM node:18-alpine
3
+
4
+ WORKDIR /app
5
+
6
+ LABEL maintainer="Wilfred Wake"
7
+ LABEL description="Orchestrator API Server for wilfredwake"
8
+
9
+ ENV NODE_ENV=production
10
+ ENV ORCHESTRATOR_PORT=3000
11
+ ENV ORCHESTRATOR_HOST=0.0.0.0
12
+ ENV PATH="/app/node_modules/.bin:$PATH"
13
+
14
+ # Install dependencies
15
+ COPY package*.json ./
16
+ RUN npm ci --only=production && npm cache clean --force
17
+
18
+ # Copy application code
19
+ COPY src/ ./src/
20
+ COPY .env* ./
21
+
22
+ # Health check
23
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
24
+ CMD node -e "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})" || exit 1
25
+
26
+ EXPOSE 3000
27
+
28
+ ENTRYPOINT ["node"]
29
+ CMD ["src/orchestrator/server.js"]
@@ -0,0 +1,74 @@
1
+ version: '3.9'
2
+
3
+ services:
4
+ # CLI container - for running wilfredwake commands
5
+ cli:
6
+ build:
7
+ context: .
8
+ dockerfile: Dockerfile
9
+ target: production
10
+ container_name: wilfredwake-cli
11
+ image: wilfredwake:latest
12
+ environment:
13
+ - NODE_ENV=production
14
+ volumes:
15
+ - ./src/config/services.yaml:/app/src/config/services.yaml
16
+ - ./.env:/app/.env
17
+ # Override to run specific commands
18
+ # command: node bin/cli.js status
19
+ # or: node bin/cli.js wake [service-name]
20
+ # or: node bin/cli.js health [service-name]
21
+ # or: node bin/cli.js init
22
+ profiles:
23
+ - cli
24
+
25
+ # Orchestrator server - for running the REST API
26
+ orchestrator:
27
+ build:
28
+ context: .
29
+ dockerfile: Dockerfile
30
+ target: production
31
+ container_name: wilfredwake-orchestrator
32
+ image: wilfredwake:latest
33
+ environment:
34
+ - NODE_ENV=production
35
+ - ORCHESTRATOR_PORT=3000
36
+ - ORCHESTRATOR_HOST=0.0.0.0
37
+ ports:
38
+ - "3000:3000"
39
+ volumes:
40
+ - ./src/config/services.yaml:/app/src/config/services.yaml
41
+ - ./.env:/app/.env
42
+ command: node src/orchestrator/server.js
43
+ healthcheck:
44
+ test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {if (r.statusCode !== 200) throw new Error(r.statusCode)})"]
45
+ interval: 30s
46
+ timeout: 10s
47
+ retries: 3
48
+ start_period: 5s
49
+ profiles:
50
+ - orchestrator
51
+ - default
52
+ networks:
53
+ - wilfredwake-network
54
+
55
+ # Development container with watch mode
56
+ dev:
57
+ build:
58
+ context: .
59
+ dockerfile: Dockerfile
60
+ target: development
61
+ container_name: wilfredwake-dev
62
+ image: wilfredwake:dev
63
+ environment:
64
+ - NODE_ENV=development
65
+ volumes:
66
+ - .:/app
67
+ - /app/node_modules
68
+ command: node --experimental-watch bin/cli.js --help
69
+ profiles:
70
+ - dev
71
+
72
+ networks:
73
+ wilfredwake-network:
74
+ driver: bridge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wilfredwake",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "CLI Tool for Multi-Developer Development Environment Wake & Status Management",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -25,7 +25,11 @@ services:
25
25
  # Frontend service
26
26
  frontend:
27
27
  url: https://transactions-k6gk.onrender.com
28
- health: /health
28
+ # Root page (/) — this is a browser page URL. Orchestrator will
29
+ # treat a failed HTTP request as LIVE if a TCP connection to the
30
+ # host:port succeeds (covers cases where the page doesn't return
31
+ # a standard HTTP status but the site is up).
32
+ health: /
29
33
  dependsOn: []
30
34
  description: "frontend Service"
31
35
 
@@ -43,13 +47,6 @@ services:
43
47
  dependsOn: []
44
48
  description: "Notification Event Consumer"
45
49
 
46
- # Notification producer
47
- notification-producer:
48
- url: https://notification-service-producer.onrender.com
49
- health: /health
50
- dependsOn: []
51
- description: "Notification Producer Service"
52
-
53
50
  # Staging environment (same services as dev)
54
51
  staging: *dev_services
55
52
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import axios from 'axios';
10
+ import net from 'net';
10
11
 
11
12
  /**
12
13
  * Service state enumeration
@@ -74,29 +75,22 @@ export class Orchestrator {
74
75
  // ASSUME ALL SERVICES ARE DEAD INITIALLY
75
76
  // NEW: Instead of /wake endpoint, just check /health repeatedly
76
77
  // ═══════════════════════════════════════════════════════════════
77
- const results = [];
78
+ const results = new Map();
79
+ const serviceStartTimes = new Map();
78
80
  const startTime = Date.now();
79
81
 
82
+ // Initialize state for each service and do an initial check
80
83
  for (const service of services) {
81
- const serviceStartTime = Date.now();
84
+ this._logTimestamp(service.name, 'Wake initiated');
85
+ this._setServiceState(service.name, ServiceState.DEAD);
86
+ this._recordLastWakeTime(service.name);
87
+ serviceStartTimes.set(service.name, Date.now());
82
88
 
83
89
  try {
84
- // NEW: Mark service as being woken and initiate health check sequence
85
- this._logTimestamp(service.name, 'Wake initiated');
90
+ const status = await this._checkHealthWithTimeout(service, Math.min(timeout, 10));
91
+ const duration = Date.now() - serviceStartTimes.get(service.name);
86
92
 
87
- // NEW: Set state to DEAD initially (always assume dead)
88
- this._setServiceState(service.name, ServiceState.DEAD);
89
- this._recordLastWakeTime(service.name);
90
-
91
- // Check health status (handles timeout internally)
92
- const status = await this._checkHealthWithTimeout(
93
- service,
94
- timeout
95
- );
96
-
97
- const duration = Date.now() - serviceStartTime;
98
-
99
- results.push({
93
+ results.set(service.name, {
100
94
  name: service.name,
101
95
  status,
102
96
  url: service.url,
@@ -107,9 +101,8 @@ export class Orchestrator {
107
101
 
108
102
  this._setServiceState(service.name, status);
109
103
  } catch (error) {
110
- const duration = Date.now() - serviceStartTime;
111
-
112
- results.push({
104
+ const duration = Date.now() - serviceStartTimes.get(service.name);
105
+ results.set(service.name, {
113
106
  name: service.name,
114
107
  status: ServiceState.FAILED,
115
108
  url: service.url,
@@ -117,18 +110,92 @@ export class Orchestrator {
117
110
  error: error.message,
118
111
  lastWakeTime: this.lastWakeTime.get(service.name),
119
112
  });
120
-
121
113
  this._setServiceState(service.name, ServiceState.FAILED);
122
114
  }
123
115
  }
124
116
 
117
+ // If caller doesn't want to wait, return early with current statuses
118
+ const toArray = () => Array.from(results.values());
119
+ let allLive = toArray().every(r => r.status === ServiceState.LIVE);
120
+ if (!wait || allLive) {
121
+ const totalDuration = Date.now() - startTime;
122
+ return {
123
+ success: allLive,
124
+ error: null,
125
+ services: toArray(),
126
+ totalDuration,
127
+ };
128
+ }
129
+
130
+ // Otherwise, poll remaining services until all are LIVE or timeout
131
+ const deadline = Date.now() + timeout * 1000;
132
+
133
+ while (Date.now() < deadline) {
134
+ // Check remaining services concurrently
135
+ const checks = [];
136
+ for (const service of services) {
137
+ const current = results.get(service.name);
138
+ if (current.status === ServiceState.LIVE) continue;
139
+
140
+ checks.push((async () => {
141
+ const health = await this._performHealthCheck(service);
142
+ if (health && health.state === ServiceState.LIVE) {
143
+ const duration = Date.now() - serviceStartTimes.get(service.name);
144
+ results.set(service.name, {
145
+ name: service.name,
146
+ status: ServiceState.LIVE,
147
+ url: service.url,
148
+ duration,
149
+ error: null,
150
+ lastWakeTime: this.lastWakeTime.get(service.name),
151
+ });
152
+ this._setServiceState(service.name, ServiceState.LIVE);
153
+ this._logTimestamp(service.name, `Became LIVE after ${duration}ms`);
154
+ } else if (health && health.state === ServiceState.DEAD) {
155
+ // still dead, update responseTime as duration
156
+ results.set(service.name, Object.assign({}, results.get(service.name), {
157
+ status: ServiceState.DEAD,
158
+ duration: Date.now() - serviceStartTimes.get(service.name),
159
+ error: health.error || results.get(service.name).error,
160
+ }));
161
+ } else if (health && health.state) {
162
+ // other intermediate state (waking/failed)
163
+ results.set(service.name, Object.assign({}, results.get(service.name), {
164
+ status: health.state,
165
+ duration: Date.now() - serviceStartTimes.get(service.name),
166
+ error: health.error || null,
167
+ }));
168
+ }
169
+ })());
170
+ }
171
+
172
+ // Wait for this round
173
+ await Promise.all(checks);
174
+
175
+ // Display progress summary
176
+ const summary = {
177
+ total: services.length,
178
+ live: toArray().filter(s => s.status === ServiceState.LIVE).length,
179
+ waking: toArray().filter(s => s.status === ServiceState.WAKING).length,
180
+ dead: toArray().filter(s => s.status === ServiceState.DEAD).length,
181
+ failed: toArray().filter(s => s.status === ServiceState.FAILED).length,
182
+ };
183
+
184
+ this._logTimestamp('orchestrator', `Progress: ${summary.live}/${summary.total} live, ${summary.waking} waking, ${summary.dead} dead, ${summary.failed} failed`);
185
+
186
+ allLive = toArray().every(r => r.status === ServiceState.LIVE);
187
+ if (allLive) break;
188
+
189
+ // small delay before next round
190
+ await this._wait(1000);
191
+ }
192
+
125
193
  const totalDuration = Date.now() - startTime;
126
- const allLive = results.every(r => r.status === ServiceState.LIVE);
127
194
 
128
195
  return {
129
- success: allLive,
196
+ success: toArray().every(r => r.status === ServiceState.LIVE),
130
197
  error: null,
131
- services: results,
198
+ services: toArray(),
132
199
  totalDuration,
133
200
  };
134
201
  }
@@ -163,6 +230,20 @@ export class Orchestrator {
163
230
  status = health.state;
164
231
  }
165
232
 
233
+ // Special rule: frontend may return an empty body or non-standard response
234
+ // that doesn't set a numeric statusCode in some environments. Per user
235
+ // requirement, treat the `frontend` service as LIVE if it returned no
236
+ // explicit error (i.e. health exists and no DEAD state). This ensures
237
+ // "nothing returned" from frontend is interpreted as live.
238
+ if (
239
+ service.name === 'frontend' &&
240
+ health &&
241
+ (health.statusCode == null) &&
242
+ health.state !== ServiceState.DEAD
243
+ ) {
244
+ status = ServiceState.LIVE;
245
+ }
246
+
166
247
  statusResults.push({
167
248
  name: service.name,
168
249
  status,
@@ -335,6 +416,55 @@ export class Orchestrator {
335
416
  service.name,
336
417
  `Health check failed: ${error.message} (DEAD - no response, needs waking)`
337
418
  );
419
+ // If this is a frontend/page-style service (health is '/'), a
420
+ // failed HTTP request can still mean the site is "live" if the
421
+ // host accepts TCP connections. Try a TCP connect to the host:port
422
+ // before marking it DEAD. This covers cases where the page itself
423
+ // doesn't return a normal HTTP status but the server is responsive
424
+ // (stops loading in a browser).
425
+ try {
426
+ if (service.health === '/' || service.health === '') {
427
+ const parsed = new URL(service.url);
428
+ const port = parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === 'https:' ? 443 : 80);
429
+ const host = parsed.hostname;
430
+
431
+ this._logTimestamp(service.name, `HTTP failed; attempting TCP connect to ${host}:${port}`);
432
+
433
+ const tcpStart = Date.now();
434
+ const tcpOk = await new Promise((resolve) => {
435
+ const socket = new net.Socket();
436
+ let settled = false;
437
+
438
+ const onCleanup = (ok) => {
439
+ if (settled) return;
440
+ settled = true;
441
+ try { socket.destroy(); } catch (e) {}
442
+ resolve(ok);
443
+ };
444
+
445
+ socket.setTimeout(3000);
446
+ socket.once('connect', () => onCleanup(true));
447
+ socket.once('timeout', () => onCleanup(false));
448
+ socket.once('error', () => onCleanup(false));
449
+ socket.connect(port, host);
450
+ });
451
+
452
+ const tcpResponseTime = Date.now() - tcpStart;
453
+
454
+ if (tcpOk) {
455
+ this._logTimestamp(service.name, `TCP connect succeeded in ${tcpResponseTime}ms - treating as LIVE`);
456
+ return {
457
+ state: ServiceState.LIVE,
458
+ statusCode: null,
459
+ responseTime: tcpResponseTime,
460
+ error: null,
461
+ uptime: null,
462
+ };
463
+ }
464
+ }
465
+ } catch (tcpErr) {
466
+ this._logTimestamp(service.name, `TCP fallback failed: ${tcpErr.message}`);
467
+ }
338
468
 
339
469
  return {
340
470
  state: ServiceState.DEAD, // No response received = service needs waking