ante-erp-cli 1.8.5 → 1.9.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.10.0] - 2025-11-04
11
+
12
+ ### Added
13
+ - **Facial-web frontend app detection and installation** in `ante update` command
14
+ - Automatic detection of missing facial-web service during updates
15
+ - Interactive prompt to install facial-web when detected as missing
16
+ - Facial-web Docker image support: `ghcr.io/gtplusnet/ante-self-hosted-frontend-facial-web`
17
+ - Port 8083 default configuration for facial-web service
18
+ - Health checks for facial-web service after installation
19
+ - Automatic docker-compose.yml update to include facial-web service
20
+ - Environment variable configuration (FACIAL_WEB_PORT, FACIAL_WEB_URL)
21
+
22
+ ### Fixed
23
+ - **CLI version auto-bumping in GitHub Actions workflow**
24
+ - Added automatic version bumping when CLI changes are pushed to main
25
+ - Support for commit message flags: `[cli:minor]`, `[cli:major]`
26
+ - Defaults to patch version bump when no flag is specified
27
+ - Auto-commits version bump with `[skip ci]` to prevent infinite loops
28
+ - Enhanced Telegram notifications to show bump type when version is auto-bumped
29
+
10
30
  ## [1.8.1] - 2025-11-01
11
31
 
12
32
  ### Fixed
package/bin/ante-cli.js CHANGED
@@ -34,6 +34,7 @@ import { migrate, seed, shell, optimize, reset as dbReset, info } from '../src/c
34
34
  import { cloneDb } from '../src/commands/clone-db.js';
35
35
  import { setDomain } from '../src/commands/set-domain.js';
36
36
  import { sslEnable, sslStatus } from '../src/commands/ssl-enable.js';
37
+ import { regenerateCompose } from '../src/commands/regenerate-compose.js';
37
38
 
38
39
  // Installation & Setup
39
40
  program
@@ -218,6 +219,11 @@ program
218
219
  .option('--no-interactive', 'Non-interactive mode')
219
220
  .action(setDomain);
220
221
 
222
+ program
223
+ .command('regenerate-compose')
224
+ .description('Regenerate docker-compose.yml with correct configuration')
225
+ .action(regenerateCompose);
226
+
221
227
  // SSL/HTTPS Management
222
228
  const sslCmd = program
223
229
  .command('ssl')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ante-erp-cli",
3
- "version": "1.8.5",
3
+ "version": "1.9.1",
4
4
  "description": "Comprehensive CLI tool for managing ANTE ERP self-hosted installations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -115,6 +115,7 @@ function showSuccess(installDir, credentials, config) {
115
115
  const apiUrl = config.apiDomain || 'http://localhost:3001';
116
116
  const gateAppUrl = config.gateAppDomain || 'http://localhost:8081';
117
117
  const guardianAppUrl = config.guardianAppDomain || 'http://localhost:8082';
118
+ const facialWebUrl = config.facialAppDomain || 'http://localhost:8083';
118
119
 
119
120
  let accessInfo = chalk.white('Access Information:') + '\n' +
120
121
  chalk.gray('━'.repeat(40)) + '\n' +
@@ -128,6 +129,10 @@ function showSuccess(installDir, credentials, config) {
128
129
  accessInfo += chalk.cyan('Guardian App: ') + chalk.white(guardianAppUrl) + '\n';
129
130
  }
130
131
 
132
+ if (config.installFacial) {
133
+ accessInfo += chalk.cyan('Facial Web: ') + chalk.white(facialWebUrl) + '\n';
134
+ }
135
+
131
136
  accessInfo += chalk.cyan('Backend: ') + chalk.white(apiUrl) + '\n' +
132
137
  chalk.cyan('WebSocket: ') + chalk.white(apiUrl) + '\n\n';
133
138
 
@@ -255,7 +260,8 @@ export async function install(options) {
255
260
  choices: [
256
261
  { name: 'Main Frontend (Required)', value: 'main', checked: true, disabled: true },
257
262
  { name: 'Gate App (School/Gate attendance)', value: 'gate', checked: false },
258
- { name: 'Guardian App (Parent portal)', value: 'guardian', checked: false }
263
+ { name: 'Guardian App (Parent portal)', value: 'guardian', checked: false },
264
+ { name: 'Facial Web (Employee face recognition)', value: 'facial', checked: false }
259
265
  ],
260
266
  validate: (answer) => {
261
267
  if (answer.length < 1) {
@@ -269,7 +275,7 @@ export async function install(options) {
269
275
  name: 'companyId',
270
276
  message: 'Company ID (for multi-tenant apps):',
271
277
  default: 1,
272
- when: (answers) => answers.frontends.includes('gate') || answers.frontends.includes('guardian'),
278
+ when: (answers) => answers.frontends.includes('gate') || answers.frontends.includes('guardian') || answers.frontends.includes('facial'),
273
279
  validate: (input) => {
274
280
  if (input < 1) return 'Company ID must be at least 1';
275
281
  return true;
@@ -356,26 +362,40 @@ export async function install(options) {
356
362
  if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
357
363
  return true;
358
364
  }
365
+ },
366
+ {
367
+ type: 'number',
368
+ name: 'facialWebPort',
369
+ message: 'Facial Web port:',
370
+ default: 8083,
371
+ when: (answers) => answers.networkType !== 'domain' && frontendConfig.frontends.includes('facial'),
372
+ validate: (input) => {
373
+ if (input < 1 || input > 65535) return 'Port must be between 1 and 65535';
374
+ return true;
375
+ }
359
376
  }
360
377
  ]);
361
378
 
362
379
  // Build URLs based on configuration
363
- let frontendUrl, apiUrl, gateAppUrl, guardianAppUrl;
380
+ let frontendUrl, apiUrl, gateAppUrl, guardianAppUrl, facialWebUrl;
364
381
  const frontendPort = networkConfig.frontendPort || 8080;
365
382
  const apiPort = networkConfig.apiPort || 3001;
366
383
  const gateAppPort = networkConfig.gateAppPort || 8081;
367
384
  const guardianAppPort = networkConfig.guardianAppPort || 8082;
385
+ const facialWebPort = networkConfig.facialWebPort || 8083;
368
386
 
369
387
  if (networkConfig.networkType === 'localhost') {
370
388
  frontendUrl = buildURL('localhost', frontendPort);
371
389
  apiUrl = buildURL('localhost', apiPort);
372
390
  gateAppUrl = buildURL('localhost', gateAppPort);
373
391
  guardianAppUrl = buildURL('localhost', guardianAppPort);
392
+ facialWebUrl = buildURL('localhost', facialWebPort);
374
393
  } else if (networkConfig.networkType === 'ip') {
375
394
  frontendUrl = buildURL(networkConfig.publicIP, frontendPort);
376
395
  apiUrl = buildURL(networkConfig.publicIP, apiPort);
377
396
  gateAppUrl = buildURL(networkConfig.publicIP, gateAppPort);
378
397
  guardianAppUrl = buildURL(networkConfig.publicIP, guardianAppPort);
398
+ facialWebUrl = buildURL(networkConfig.publicIP, facialWebPort);
379
399
  } else {
380
400
  // Domain - use standard HTTP/HTTPS ports (NGINX will handle reverse proxy)
381
401
  const isHttps = await inquirer.prompt([{
@@ -403,6 +423,7 @@ export async function install(options) {
403
423
  apiUrl = `${protocol}://${apiSubdomain.subdomain}.${networkConfig.domainName}`;
404
424
  gateAppUrl = `${protocol}://gate.${networkConfig.domainName}`;
405
425
  guardianAppUrl = `${protocol}://guardian.${networkConfig.domainName}`;
426
+ facialWebUrl = `${protocol}://facial.${networkConfig.domainName}`;
406
427
  }
407
428
 
408
429
  config = {
@@ -412,12 +433,15 @@ export async function install(options) {
412
433
  apiDomain: apiUrl,
413
434
  gateAppDomain: gateAppUrl,
414
435
  guardianAppDomain: guardianAppUrl,
436
+ facialAppDomain: facialWebUrl,
415
437
  frontendPort,
416
438
  apiPort,
417
439
  gateAppPort,
418
440
  guardianAppPort,
441
+ facialWebPort,
419
442
  installGate: frontendConfig.frontends.includes('gate'),
420
- installGuardian: frontendConfig.frontends.includes('guardian')
443
+ installGuardian: frontendConfig.frontends.includes('guardian'),
444
+ installFacial: frontendConfig.frontends.includes('facial')
421
445
  };
422
446
  } else {
423
447
  // Non-interactive mode - use options or defaults with IP detection
@@ -488,6 +512,9 @@ export async function install(options) {
488
512
  if (config.installGuardian) {
489
513
  console.log(chalk.cyan(' Guardian App:'), chalk.white(config.guardianAppDomain));
490
514
  }
515
+ if (config.installFacial) {
516
+ console.log(chalk.cyan(' Facial Web:'), chalk.white(config.facialAppDomain));
517
+ }
491
518
  console.log(chalk.cyan(' API:'), chalk.white(config.apiDomain));
492
519
  if (config.companyId) {
493
520
  console.log(chalk.cyan(' Company ID:'), chalk.white(config.companyId));
@@ -557,11 +584,14 @@ export async function install(options) {
557
584
  backendPort: config.apiPort || 3001,
558
585
  gateAppPort: config.gateAppPort || 8081,
559
586
  guardianAppPort: config.guardianAppPort || 8082,
587
+ facialWebPort: config.facialWebPort || 8083,
560
588
  gateAppUrl: config.gateAppDomain || 'http://localhost:8081',
561
589
  guardianAppUrl: config.guardianAppDomain || 'http://localhost:8082',
590
+ facialWebUrl: config.facialWebUrl || 'http://localhost:8083',
562
591
  companyId: config.companyId || 1,
563
592
  installGate: config.installGate || false,
564
- installGuardian: config.installGuardian || false
593
+ installGuardian: config.installGuardian || false,
594
+ installFacial: config.installFacial || false
565
595
  });
566
596
 
567
597
  writeFileSync(join(config.installDir, 'docker-compose.yml'), dockerCompose);
@@ -0,0 +1,141 @@
1
+ import chalk from 'chalk';
2
+ import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { getInstallDir } from '../utils/config.js';
5
+ import { generateDockerCompose } from '../templates/docker-compose.yml.js';
6
+
7
+ /**
8
+ * Parse .env file to extract configuration
9
+ * @param {string} envPath - Path to .env file
10
+ * @returns {Object} Configuration object
11
+ */
12
+ function parseEnvFile(envPath) {
13
+ const envContent = readFileSync(envPath, 'utf8');
14
+ const config = {};
15
+
16
+ envContent.split('\n').forEach(line => {
17
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
18
+ if (match) {
19
+ config[match[1]] = match[2];
20
+ }
21
+ });
22
+
23
+ return config;
24
+ }
25
+
26
+ /**
27
+ * Detect which apps are installed from existing docker-compose.yml
28
+ * @param {string} composeFile - Path to docker-compose.yml
29
+ * @returns {Object} Installed apps
30
+ */
31
+ function detectInstalledApps(composeFile) {
32
+ try {
33
+ const composeContent = readFileSync(composeFile, 'utf8');
34
+ return {
35
+ hasMain: composeContent.includes('frontend:') || composeContent.includes('container_name: ante-frontend'),
36
+ hasGateApp: composeContent.includes('gate-app:') || composeContent.includes('container_name: ante-gate-app'),
37
+ hasGuardianApp: composeContent.includes('guardian-app:') || composeContent.includes('container_name: ante-guardian-app')
38
+ };
39
+ } catch {
40
+ return { hasMain: true, hasGateApp: false, hasGuardianApp: false };
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Regenerate docker-compose.yml with correct configuration
46
+ */
47
+ export async function regenerateCompose() {
48
+ try {
49
+ console.log(chalk.bold('\n🔧 Regenerate docker-compose.yml\n'));
50
+
51
+ const installDir = getInstallDir();
52
+ const composeFile = join(installDir, 'docker-compose.yml');
53
+ const envFile = join(installDir, '.env');
54
+
55
+ console.log(chalk.gray(`Installation: ${installDir}\n`));
56
+
57
+ // Check if files exist
58
+ if (!existsSync(composeFile)) {
59
+ console.log(chalk.red('✗ docker-compose.yml not found'));
60
+ console.log(chalk.gray(' Run "ante install" first\n'));
61
+ process.exit(1);
62
+ }
63
+
64
+ if (!existsSync(envFile)) {
65
+ console.log(chalk.red('✗ .env file not found'));
66
+ console.log(chalk.gray(' Run "ante install" first\n'));
67
+ process.exit(1);
68
+ }
69
+
70
+ // Detect installed apps
71
+ const installed = detectInstalledApps(composeFile);
72
+ console.log(chalk.cyan('Detected Configuration:'));
73
+ console.log(chalk.gray(` Frontend Main: ${installed.hasMain ? '✓ Installed' : '✗ Not installed'}`));
74
+ console.log(chalk.gray(` Gate App: ${installed.hasGateApp ? '✓ Installed' : '✗ Not installed'}`));
75
+ console.log(chalk.gray(` Guardian App: ${installed.hasGuardianApp ? '✓ Installed' : '✗ Not installed'}\n`));
76
+
77
+ // Parse .env file
78
+ const envConfig = parseEnvFile(envFile);
79
+
80
+ // Extract port configuration
81
+ const frontendPort = parseInt(envConfig.FRONTEND_PORT) || 8080;
82
+ const backendPort = parseInt(envConfig.BACKEND_PORT) || 3001;
83
+ const gateAppPort = parseInt(envConfig.GATE_APP_PORT) || 8081;
84
+ const guardianAppPort = parseInt(envConfig.GUARDIAN_APP_PORT) || 8082;
85
+ const companyId = parseInt(envConfig.COMPANY_ID) || 1;
86
+
87
+ console.log(chalk.cyan('Port Configuration:'));
88
+ console.log(chalk.gray(` Frontend: ${frontendPort}`));
89
+ console.log(chalk.gray(` Backend: ${backendPort}`));
90
+ if (installed.hasGateApp) {
91
+ console.log(chalk.gray(` Gate App: ${gateAppPort}`));
92
+ }
93
+ if (installed.hasGuardianApp) {
94
+ console.log(chalk.gray(` Guardian: ${guardianAppPort}`));
95
+ }
96
+ console.log(chalk.gray(` Company ID: ${companyId}\n`));
97
+
98
+ // Backup existing docker-compose.yml
99
+ const timestamp = new Date().toISOString().split('.')[0].replace(/:/g, '-').replace('T', '_');
100
+ const backupFile = `${composeFile}.${timestamp}.bak`;
101
+
102
+ console.log(chalk.gray('📦 Creating backup...'));
103
+ renameSync(composeFile, backupFile);
104
+ console.log(chalk.green(`✓ Backup created: ${backupFile}\n`));
105
+
106
+ // Generate new docker-compose.yml
107
+ console.log(chalk.gray('⚙️ Generating new docker-compose.yml...'));
108
+ const newCompose = generateDockerCompose({
109
+ frontendPort,
110
+ backendPort,
111
+ gateAppPort,
112
+ guardianAppPort,
113
+ installMain: installed.hasMain,
114
+ installGate: installed.hasGateApp,
115
+ installGuardian: installed.hasGuardianApp,
116
+ companyId
117
+ });
118
+
119
+ // Write new docker-compose.yml
120
+ writeFileSync(composeFile, newCompose);
121
+ console.log(chalk.green('✓ New docker-compose.yml generated\n'));
122
+
123
+ // Show what changed
124
+ console.log(chalk.bold.green('✓ Configuration regenerated successfully!\n'));
125
+ console.log(chalk.cyan('Next Steps:'));
126
+ console.log(chalk.gray(' 1. Review the changes: diff docker-compose.yml ' + backupFile.replace(installDir + '/', '')));
127
+ console.log(chalk.gray(' 2. Restart services: ante restart'));
128
+ console.log(chalk.gray(' 3. Check status: ante status\n'));
129
+
130
+ // Show important fixes
131
+ if (installed.hasGuardianApp) {
132
+ console.log(chalk.yellow('⚠ Important: Guardian App port mapping updated'));
133
+ console.log(chalk.gray(` Old: ${guardianAppPort}:3000 (incorrect)`));
134
+ console.log(chalk.gray(` New: ${guardianAppPort}:9003 (correct)\n`));
135
+ }
136
+
137
+ } catch (error) {
138
+ console.error(chalk.red('\n✗ Failed to regenerate docker-compose.yml:'), error.message);
139
+ process.exit(1);
140
+ }
141
+ }
@@ -9,32 +9,34 @@ import { backup } from './backup.js';
9
9
  import { generateDockerCompose } from '../templates/docker-compose.yml.js';
10
10
 
11
11
  /**
12
- * Check if gate-app or guardian-app is installed by checking docker-compose.yml
12
+ * Check if gate-app, guardian-app, or facial-web is installed by checking docker-compose.yml
13
13
  * @param {string} composeFile - Path to docker-compose.yml
14
- * @returns {{hasGateApp: boolean, hasGuardianApp: boolean}}
14
+ * @returns {{hasGateApp: boolean, hasGuardianApp: boolean, hasFacialWeb: boolean}}
15
15
  */
16
16
  function detectInstalledApps(composeFile) {
17
17
  try {
18
18
  const composeContent = readFileSync(composeFile, 'utf8');
19
19
  return {
20
20
  hasGateApp: composeContent.includes('gate-app:') || composeContent.includes('container_name: ante-gate-app'),
21
- hasGuardianApp: composeContent.includes('guardian-app:') || composeContent.includes('container_name: ante-guardian-app')
21
+ hasGuardianApp: composeContent.includes('guardian-app:') || composeContent.includes('container_name: ante-guardian-app'),
22
+ hasFacialWeb: composeContent.includes('facial-web:') || composeContent.includes('container_name: ante-facial-web')
22
23
  };
23
24
  } catch {
24
- return { hasGateApp: false, hasGuardianApp: false };
25
+ return { hasGateApp: false, hasGuardianApp: false, hasFacialWeb: false };
25
26
  }
26
27
  }
27
28
 
28
29
  /**
29
30
  * Detect missing services that could be installed
30
31
  * @param {string} composeFile - Path to docker-compose.yml
31
- * @returns {{gateApp: boolean, guardianApp: boolean}}
32
+ * @returns {{gateApp: boolean, guardianApp: boolean, facialWeb: boolean}}
32
33
  */
33
34
  function detectMissingServices(composeFile) {
34
35
  const installed = detectInstalledApps(composeFile);
35
36
  return {
36
37
  gateApp: !installed.hasGateApp,
37
- guardianApp: !installed.hasGuardianApp
38
+ guardianApp: !installed.hasGuardianApp,
39
+ facialWeb: !installed.hasFacialWeb
38
40
  };
39
41
  }
40
42
 
@@ -61,7 +63,7 @@ function parseEnvFile(envFile) {
61
63
  * Update docker-compose.yml with new services
62
64
  * @param {string} composeFile - Path to docker-compose.yml
63
65
  * @param {string} envFile - Path to .env file
64
- * @param {{gateApp: boolean, guardianApp: boolean}} newServices - Services to add
66
+ * @param {{gateApp: boolean, guardianApp: boolean, facialWeb: boolean}} newServices - Services to add
65
67
  */
66
68
  function updateDockerCompose(composeFile, envFile, newServices) {
67
69
  const envConfig = parseEnvFile(envFile);
@@ -73,9 +75,11 @@ function updateDockerCompose(composeFile, envFile, newServices) {
73
75
  backendPort: parseInt(envConfig.BACKEND_PORT) || 3001,
74
76
  gateAppPort: parseInt(envConfig.GATE_APP_PORT) || 8081,
75
77
  guardianAppPort: parseInt(envConfig.GUARDIAN_APP_PORT) || 8082,
78
+ facialWebPort: parseInt(envConfig.FACIAL_WEB_PORT) || 8083,
76
79
  installMain: true,
77
80
  installGate: currentInstalled.hasGateApp || newServices.gateApp,
78
81
  installGuardian: currentInstalled.hasGuardianApp || newServices.guardianApp,
82
+ installFacial: currentInstalled.hasFacialWeb || newServices.facialWeb,
79
83
  companyId: parseInt(envConfig.COMPANY_ID) || 1
80
84
  });
81
85
 
@@ -90,7 +94,7 @@ function updateDockerCompose(composeFile, envFile, newServices) {
90
94
  /**
91
95
  * Update .env file with new app configuration
92
96
  * @param {string} envFile - Path to .env file
93
- * @param {{gateApp: boolean, guardianApp: boolean}} newServices - Services to add
97
+ * @param {{gateApp: boolean, guardianApp: boolean, facialWeb: boolean}} newServices - Services to add
94
98
  */
95
99
  function updateEnvFile(envFile, newServices) {
96
100
  let envContent = readFileSync(envFile, 'utf8');
@@ -143,8 +147,32 @@ COMPANY_ID=1
143
147
  }
144
148
  }
145
149
 
150
+ // Add facial-web configuration if needed
151
+ if (newServices.facialWeb && !envContent.includes('FACIAL_WEB_PORT')) {
152
+ if (!envContent.includes('# FRONTEND APP CONFIGURATION')) {
153
+ envContent += `\n# ------------------------------------------------------------------------------
154
+ # FRONTEND APP CONFIGURATION
155
+ # ------------------------------------------------------------------------------
156
+ COMPANY_ID=1
157
+ `;
158
+ }
159
+
160
+ // Replace commented line or add new
161
+ if (envContent.includes('# FACIAL_WEB_PORT=')) {
162
+ envContent = envContent.replace(/# FACIAL_WEB_PORT=\d+/, 'FACIAL_WEB_PORT=8083');
163
+ } else {
164
+ envContent += `FACIAL_WEB_PORT=8083\n`;
165
+ }
166
+
167
+ if (envContent.includes('# FACIAL_WEB_URL=')) {
168
+ envContent = envContent.replace(/# FACIAL_WEB_URL=.*/, 'FACIAL_WEB_URL=http://localhost:8083');
169
+ } else {
170
+ envContent += `FACIAL_WEB_URL=http://localhost:8083\n`;
171
+ }
172
+ }
173
+
146
174
  // Add COMPANY_ID if missing and any new service is being added
147
- if ((newServices.gateApp || newServices.guardianApp) && !envContent.includes('COMPANY_ID=')) {
175
+ if ((newServices.gateApp || newServices.guardianApp || newServices.facialWeb) && !envContent.includes('COMPANY_ID=')) {
148
176
  envContent += `COMPANY_ID=1\n`;
149
177
  }
150
178
 
@@ -161,7 +189,7 @@ export async function update(options) {
161
189
  const envFile = join(installDir, '.env');
162
190
 
163
191
  // Detect which apps are installed
164
- const { hasGateApp, hasGuardianApp } = detectInstalledApps(composeFile);
192
+ const { hasGateApp, hasGuardianApp, hasFacialWeb } = detectInstalledApps(composeFile);
165
193
 
166
194
  console.log(chalk.bold('\n🔄 ANTE ERP Update\n'));
167
195
 
@@ -191,7 +219,7 @@ export async function update(options) {
191
219
  }
192
220
 
193
221
  // Track which services to install
194
- let servicesToInstall = { gateApp: false, guardianApp: false };
222
+ let servicesToInstall = { gateApp: false, guardianApp: false, facialWeb: false };
195
223
 
196
224
  const tasks = new Listr([
197
225
  {
@@ -207,7 +235,7 @@ export async function update(options) {
207
235
  task: async (ctx, task) => {
208
236
  const missingServices = detectMissingServices(composeFile);
209
237
 
210
- if (!missingServices.gateApp && !missingServices.guardianApp) {
238
+ if (!missingServices.gateApp && !missingServices.guardianApp && !missingServices.facialWeb) {
211
239
  task.skip('All available services are already installed');
212
240
  return;
213
241
  }
@@ -215,6 +243,7 @@ export async function update(options) {
215
243
  const availableServices = [];
216
244
  if (missingServices.gateApp) availableServices.push('Gate App (School attendance)');
217
245
  if (missingServices.guardianApp) availableServices.push('Guardian App (Parent portal)');
246
+ if (missingServices.facialWeb) availableServices.push('Facial Web (Face recognition)');
218
247
 
219
248
  task.output = `Found ${availableServices.length} new service(s): ${availableServices.join(', ')}`;
220
249
  ctx.missingServices = missingServices;
@@ -222,7 +251,7 @@ export async function update(options) {
222
251
  },
223
252
  {
224
253
  title: 'Confirming installation of new services',
225
- skip: (ctx) => !ctx.missingServices || (!ctx.missingServices.gateApp && !ctx.missingServices.guardianApp),
254
+ skip: (ctx) => !ctx.missingServices || (!ctx.missingServices.gateApp && !ctx.missingServices.guardianApp && !ctx.missingServices.facialWeb),
226
255
  task: async (ctx, task) => {
227
256
  if (options.force) {
228
257
  servicesToInstall = ctx.missingServices;
@@ -238,12 +267,13 @@ export async function update(options) {
238
267
  const availableServices = [];
239
268
  if (ctx.missingServices.gateApp) availableServices.push('Gate App');
240
269
  if (ctx.missingServices.guardianApp) availableServices.push('Guardian App');
270
+ if (ctx.missingServices.facialWeb) availableServices.push('Facial Web');
241
271
 
242
272
  const { installNew } = await inquirer.prompt([
243
273
  {
244
274
  type: 'confirm',
245
275
  name: 'installNew',
246
- message: `Install ${availableServices.join(' and ')}?`,
276
+ message: `Install ${availableServices.join(', ')}?`,
247
277
  default: true
248
278
  }
249
279
  ]);
@@ -258,11 +288,12 @@ export async function update(options) {
258
288
  },
259
289
  {
260
290
  title: 'Installing new services',
261
- skip: () => !servicesToInstall.gateApp && !servicesToInstall.guardianApp,
291
+ skip: () => !servicesToInstall.gateApp && !servicesToInstall.guardianApp && !servicesToInstall.facialWeb,
262
292
  task: async (ctx, task) => {
263
293
  const servicesAdded = [];
264
294
  if (servicesToInstall.gateApp) servicesAdded.push('Gate App');
265
295
  if (servicesToInstall.guardianApp) servicesAdded.push('Guardian App');
296
+ if (servicesToInstall.facialWeb) servicesAdded.push('Facial Web');
266
297
 
267
298
  task.output = `Adding ${servicesAdded.join(', ')} to configuration...`;
268
299
 
@@ -322,6 +353,16 @@ export async function update(options) {
322
353
  }
323
354
  }
324
355
  },
356
+ {
357
+ title: 'Waiting for Facial Web to be healthy',
358
+ skip: () => !hasFacialWeb && !servicesToInstall.facialWeb,
359
+ task: async () => {
360
+ const healthy = await waitForServiceHealthy(composeFile, 'facial-web', 60);
361
+ if (!healthy) {
362
+ throw new Error('Facial Web did not become healthy within 60 seconds');
363
+ }
364
+ }
365
+ },
325
366
  {
326
367
  title: 'Running database migrations',
327
368
  task: async (ctx, task) => {
@@ -9,8 +9,10 @@ export function generateDockerCompose(options = {}) {
9
9
  backendPort = 3001,
10
10
  gateAppPort = 8081,
11
11
  guardianAppPort = 8082,
12
+ facialWebPort = 8083,
12
13
  installGate = false,
13
14
  installGuardian = false,
15
+ installFacial = false,
14
16
  companyId = 1
15
17
  } = options;
16
18
 
@@ -240,6 +242,33 @@ ${installGate ? `
240
242
  options:
241
243
  max-size: "10m"
242
244
  max-file: "3"
245
+ ` : ''}${installFacial ? `
246
+ # ANTE Facial Web
247
+ facial-web:
248
+ image: ghcr.io/gtplusnet/ante-self-hosted-frontend-facial-web:latest
249
+ container_name: ante-facial-web
250
+ restart: unless-stopped
251
+ depends_on:
252
+ backend:
253
+ condition: service_healthy
254
+ environment:
255
+ - API_URL=\${API_URL:-http://localhost:${backendPort}}
256
+ - SOCKET_URL=\${SOCKET_URL:-ws://localhost:${backendPort}}
257
+ - COMPANY_ID=${companyId}
258
+ ports:
259
+ - "${facialWebPort}:5173"
260
+ networks:
261
+ - ante-network
262
+ healthcheck:
263
+ test: ["CMD", "curl", "-f", "http://localhost:5173"]
264
+ interval: 30s
265
+ timeout: 10s
266
+ retries: 3
267
+ logging:
268
+ driver: "json-file"
269
+ options:
270
+ max-size: "10m"
271
+ max-file: "3"
243
272
  ` : ''}
244
273
  volumes:
245
274
  postgres_data:
@@ -13,11 +13,14 @@ export function generateEnv(credentials, options = {}) {
13
13
  backendPort = 3001,
14
14
  gateAppPort = 8081,
15
15
  guardianAppPort = 8082,
16
+ facialWebPort = 8083,
16
17
  gateAppUrl = 'http://localhost:8081',
17
18
  guardianAppUrl = 'http://localhost:8082',
19
+ facialWebUrl = 'http://localhost:8083',
18
20
  companyId = 1,
19
21
  installGate = false,
20
22
  installGuardian = false,
23
+ installFacial = false,
21
24
  smtpHost = '',
22
25
  smtpPort = 587,
23
26
  smtpUsername = '',
@@ -60,14 +63,16 @@ FRONTEND_PORT=${frontendPort}
60
63
  BACKEND_PORT=${backendPort}
61
64
  ${installGate ? `GATE_APP_PORT=${gateAppPort}` : '# GATE_APP_PORT=8081'}
62
65
  ${installGuardian ? `GUARDIAN_APP_PORT=${guardianAppPort}` : '# GUARDIAN_APP_PORT=8082'}
66
+ ${installFacial ? `FACIAL_WEB_PORT=${facialWebPort}` : '# FACIAL_WEB_PORT=8083'}
63
67
  # Note: WebSocket runs on the same port as backend (BACKEND_PORT)
64
68
 
65
- ${(installGate || installGuardian) ? `# ------------------------------------------------------------------------------
69
+ ${(installGate || installGuardian || installFacial) ? `# ------------------------------------------------------------------------------
66
70
  # FRONTEND APP CONFIGURATION
67
71
  # ------------------------------------------------------------------------------
68
72
  COMPANY_ID=${companyId}
69
73
  ${installGate ? `GATE_APP_URL=${gateAppUrl}` : '# GATE_APP_URL=http://localhost:8081'}
70
74
  ${installGuardian ? `GUARDIAN_APP_URL=${guardianAppUrl}` : '# GUARDIAN_APP_URL=http://localhost:8082'}
75
+ ${installFacial ? `FACIAL_WEB_URL=${facialWebUrl}` : '# FACIAL_WEB_URL=http://localhost:8083'}
71
76
 
72
77
  ` : ''}# ------------------------------------------------------------------------------
73
78
  # EMAIL CONFIGURATION
@@ -50,10 +50,12 @@ export async function installNginx(spinner) {
50
50
  * @param {string} config.apiDomain - API domain (e.g., api.ante.example.com)
51
51
  * @param {string} [config.gateAppDomain] - Gate app domain (optional)
52
52
  * @param {string} [config.guardianAppDomain] - Guardian app domain (optional)
53
+ * @param {string} [config.facialAppDomain] - Facial web app domain (optional)
53
54
  * @param {number} config.frontendPort - Frontend Docker port (default: 8080)
54
55
  * @param {number} config.apiPort - API Docker port (default: 3001)
55
56
  * @param {number} [config.gateAppPort] - Gate app Docker port (default: 8081)
56
57
  * @param {number} [config.guardianAppPort] - Guardian app Docker port (default: 8082)
58
+ * @param {number} [config.facialWebPort] - Facial web app Docker port (default: 8083)
57
59
  * @param {boolean} config.ssl - Enable SSL configuration (default: false)
58
60
  * @returns {string} NGINX configuration content
59
61
  */
@@ -63,10 +65,12 @@ export function generateNginxConfig(config) {
63
65
  apiDomain,
64
66
  gateAppDomain,
65
67
  guardianAppDomain,
68
+ facialAppDomain,
66
69
  frontendPort = 8080,
67
70
  apiPort = 3001,
68
71
  gateAppPort = 8081,
69
72
  guardianAppPort = 8082,
73
+ facialWebPort = 8083,
70
74
  ssl = false
71
75
  } = config;
72
76
 
@@ -75,6 +79,7 @@ export function generateNginxConfig(config) {
75
79
  const apiHost = apiDomain.replace(/^https?:\/\//, '').replace(/:\d+$/, '');
76
80
  const gateAppHost = gateAppDomain ? gateAppDomain.replace(/^https?:\/\//, '').replace(/:\d+$/, '') : null;
77
81
  const guardianAppHost = guardianAppDomain ? guardianAppDomain.replace(/^https?:\/\//, '').replace(/:\d+$/, '') : null;
82
+ const facialAppHost = facialAppDomain ? facialAppDomain.replace(/^https?:\/\//, '').replace(/:\d+$/, '') : null;
78
83
 
79
84
  if (ssl) {
80
85
  return generateSslNginxConfig({
@@ -82,10 +87,12 @@ export function generateNginxConfig(config) {
82
87
  apiHost,
83
88
  gateAppHost,
84
89
  guardianAppHost,
90
+ facialAppHost,
85
91
  frontendPort,
86
92
  apiPort,
87
93
  gateAppPort,
88
- guardianAppPort
94
+ guardianAppPort,
95
+ facialWebPort
89
96
  });
90
97
  }
91
98
 
@@ -216,6 +223,37 @@ server {
216
223
  proxy_read_timeout 60s;
217
224
  }
218
225
  }
226
+ ` : ''}${facialAppHost ? `
227
+ # ANTE Facial Web App Configuration
228
+ server {
229
+ listen 80;
230
+ listen [::]:80;
231
+ server_name ${facialAppHost};
232
+
233
+ # Increase buffer sizes for large headers
234
+ client_header_buffer_size 16k;
235
+ large_client_header_buffers 4 16k;
236
+
237
+ # Increase body size for image uploads
238
+ client_max_body_size 50M;
239
+
240
+ location / {
241
+ proxy_pass http://localhost:${facialWebPort};
242
+ proxy_http_version 1.1;
243
+ proxy_set_header Upgrade $http_upgrade;
244
+ proxy_set_header Connection 'upgrade';
245
+ proxy_set_header Host $host;
246
+ proxy_set_header X-Real-IP $remote_addr;
247
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
248
+ proxy_set_header X-Forwarded-Proto $scheme;
249
+ proxy_cache_bypass $http_upgrade;
250
+
251
+ # Timeout settings
252
+ proxy_connect_timeout 60s;
253
+ proxy_send_timeout 60s;
254
+ proxy_read_timeout 60s;
255
+ }
256
+ }
219
257
  ` : ''}`;
220
258
  }
221
259
 
@@ -226,10 +264,12 @@ server {
226
264
  * @param {string} config.apiHost - API hostname
227
265
  * @param {string} [config.gateAppHost] - Gate app hostname (optional)
228
266
  * @param {string} [config.guardianAppHost] - Guardian app hostname (optional)
267
+ * @param {string} [config.facialAppHost] - Facial web app hostname (optional)
229
268
  * @param {number} config.frontendPort - Frontend port
230
269
  * @param {number} config.apiPort - API port
231
270
  * @param {number} [config.gateAppPort] - Gate app port (default: 8081)
232
271
  * @param {number} [config.guardianAppPort] - Guardian app port (default: 8082)
272
+ * @param {number} [config.facialWebPort] - Facial web app port (default: 8083)
233
273
  * @returns {string} NGINX configuration with SSL
234
274
  */
235
275
  function generateSslNginxConfig(config) {
@@ -238,10 +278,12 @@ function generateSslNginxConfig(config) {
238
278
  apiHost,
239
279
  gateAppHost,
240
280
  guardianAppHost,
281
+ facialAppHost,
241
282
  frontendPort,
242
283
  apiPort,
243
284
  gateAppPort = 8081,
244
- guardianAppPort = 8082
285
+ guardianAppPort = 8082,
286
+ facialWebPort = 8083
245
287
  } = config;
246
288
 
247
289
  return `# ANTE Frontend Configuration (HTTPS)
@@ -475,6 +517,63 @@ server {
475
517
  server_name ${guardianAppHost};
476
518
  return 301 https://$server_name$request_uri;
477
519
  }
520
+ ` : ''}${facialAppHost ? `
521
+ # ANTE Facial Web App Configuration (HTTPS)
522
+ server {
523
+ listen 443 ssl;
524
+ listen [::]:443 ssl;
525
+ http2 on;
526
+ server_name ${facialAppHost};
527
+
528
+ # SSL Certificate paths
529
+ ssl_certificate /etc/letsencrypt/live/${facialAppHost}/fullchain.pem;
530
+ ssl_certificate_key /etc/letsencrypt/live/${facialAppHost}/privkey.pem;
531
+
532
+ # SSL Configuration
533
+ ssl_protocols TLSv1.2 TLSv1.3;
534
+ ssl_ciphers HIGH:!aNULL:!MD5;
535
+ ssl_prefer_server_ciphers on;
536
+ ssl_session_cache shared:SSL:10m;
537
+ ssl_session_timeout 10m;
538
+
539
+ # Security headers
540
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
541
+ add_header X-Frame-Options SAMEORIGIN always;
542
+ add_header X-Content-Type-Options nosniff always;
543
+ add_header X-XSS-Protection "1; mode=block" always;
544
+
545
+ # Increase buffer sizes for large headers
546
+ client_header_buffer_size 16k;
547
+ large_client_header_buffers 4 16k;
548
+
549
+ # Increase body size for image uploads
550
+ client_max_body_size 50M;
551
+
552
+ location / {
553
+ proxy_pass http://localhost:${facialWebPort};
554
+ proxy_http_version 1.1;
555
+ proxy_set_header Upgrade $http_upgrade;
556
+ proxy_set_header Connection 'upgrade';
557
+ proxy_set_header Host $host;
558
+ proxy_set_header X-Real-IP $remote_addr;
559
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
560
+ proxy_set_header X-Forwarded-Proto $scheme;
561
+ proxy_cache_bypass $http_upgrade;
562
+
563
+ # Timeout settings
564
+ proxy_connect_timeout 60s;
565
+ proxy_send_timeout 60s;
566
+ proxy_read_timeout 60s;
567
+ }
568
+ }
569
+
570
+ # HTTP to HTTPS redirect for ${facialAppHost}
571
+ server {
572
+ listen 80;
573
+ listen [::]:80;
574
+ server_name ${facialAppHost};
575
+ return 301 https://$server_name$request_uri;
576
+ }
478
577
  ` : ''}`;
479
578
  }
480
579
 
@@ -551,6 +650,9 @@ export async function configureNginx(config) {
551
650
  if (config.guardianAppDomain) {
552
651
  console.log(chalk.gray(` Guardian App: ${config.guardianAppDomain} → localhost:${config.guardianAppPort || 8082}`));
553
652
  }
653
+ if (config.facialAppDomain) {
654
+ console.log(chalk.gray(` Facial Web: ${config.facialAppDomain} → localhost:${config.facialWebPort || 8083}`));
655
+ }
554
656
  console.log(chalk.gray(` API: ${config.apiDomain} → localhost:${config.apiPort || 3001}`));
555
657
 
556
658
  } catch (error) {