import 'package:uuid/uuid.dart'; /// What kind of thing this service deploys. enum ServiceKind { jarService, webApp, generic } /// Front-end framework for a [ServiceKind.webApp]. enum WebFramework { angular, react, flutterWeb, staticFiles } /// How a service's health is probed. enum HealthCheckType { auto, http, tcp, command, none } extension ServiceKindX on ServiceKind { String get label => switch (this) { ServiceKind.webApp => 'Web app (nginx)', ServiceKind.generic => 'Service (any binary)', ServiceKind.jarService => 'Jar service', }; } extension HealthCheckTypeX on HealthCheckType { String get label => switch (this) { HealthCheckType.auto => 'systemd (auto)', HealthCheckType.http => 'HTTP request', HealthCheckType.tcp => 'TCP port', HealthCheckType.command => 'Command exit 0', HealthCheckType.none => 'Disabled', }; } extension WebFrameworkX on WebFramework { String get label => switch (this) { WebFramework.angular => 'Angular', WebFramework.react => 'React', WebFramework.flutterWeb => 'Flutter web', WebFramework.staticFiles => 'Static files', }; /// Default build command (run through a shell). Blank = no build. String get defaultBuild => switch (this) { WebFramework.angular => 'npm install && npm run build', WebFramework.react => 'npm install && npm run build', WebFramework.flutterWeb => 'flutter build web --release', WebFramework.staticFiles => '', }; } /// A single deployable service that lives on a [ServerProfile]. /// One server can host many of these — jar services and/or web apps. class ServiceConfig { final String id; String name; // friendly label, e.g. "orders-api" ServiceKind kind; WebFramework framework; /// Absolute target directory on the server. /// - jar service: where the jar lives, e.g. /opt/orders /// - web app: the nginx web root, e.g. /var/www/orders String remoteDir; /// The filename the uploaded jar should be saved as (jar services only). String remoteJarName; /// The systemd unit to (re)start for a jar service, e.g. "orders". String serviceName; /// The nginx unit to reload for a web app (web only). Default "nginx". String nginxService; /// Optional custom log command (WITHOUT sudo — the deployer adds it). String logCommand; /// Absolute path to the local source project. When set, PeachDeployments can build /// from source before deploying. String localProjectPath; /// Optional custom build command. Blank auto-detects. String buildCommand; /// jar services: glob/path for the built jar (blank auto-detects). String jarPattern; /// web apps: build output directory relative to the project (blank /// auto-detects, e.g. dist/, build/, build/web). String outputSubdir; /// How to probe this service's health. HealthCheckType healthType; /// Health target: a URL (http), port number (tcp), or shell command. String healthTarget; ServiceConfig({ String? id, this.name = 'New service', this.kind = ServiceKind.jarService, this.framework = WebFramework.react, this.remoteDir = '/opt/app', this.remoteJarName = '', this.serviceName = '', this.nginxService = 'nginx', this.logCommand = '', this.localProjectPath = '', this.buildCommand = '', this.jarPattern = '', this.outputSubdir = '', this.healthType = HealthCheckType.auto, this.healthTarget = '', }) : id = id ?? const Uuid().v4(); bool get isWeb => kind == ServiceKind.webApp; bool get isGeneric => kind == ServiceKind.generic; bool get canBuild => localProjectPath.trim().isNotEmpty; String get effectiveLogCommand { if (logCommand.trim().isNotEmpty) return logCommand.trim(); if (isWeb) { final unit = nginxService.trim().isEmpty ? 'nginx' : nginxService.trim(); return 'journalctl -u $unit -n 200 --no-pager'; } return 'journalctl -u ${serviceName.isEmpty ? 'app' : serviceName} ' '-n 200 --no-pager'; } /// Normalized remote directory (no trailing slash). String get remoteRoot => remoteDir.endsWith('/') ? remoteDir.substring(0, remoteDir.length - 1) : remoteDir; String remotePath(String jarLabelFallback) { final name = remoteJarName.trim().isNotEmpty ? remoteJarName.trim() : jarLabelFallback; return '$remoteRoot/$name'; } ServiceConfig copy() => ServiceConfig.fromJson(toJson()); Map toJson() => { 'id': id, 'name': name, 'kind': kind.name, 'framework': framework.name, 'remoteDir': remoteDir, 'remoteJarName': remoteJarName, 'serviceName': serviceName, 'nginxService': nginxService, 'logCommand': logCommand, 'localProjectPath': localProjectPath, 'buildCommand': buildCommand, 'jarPattern': jarPattern, 'outputSubdir': outputSubdir, 'healthType': healthType.name, 'healthTarget': healthTarget, }; factory ServiceConfig.fromJson(Map j) => ServiceConfig( id: j['id'] as String?, name: (j['name'] ?? 'service') as String, kind: ServiceKind.values.firstWhere( (k) => k.name == j['kind'], orElse: () => ServiceKind.jarService, ), framework: WebFramework.values.firstWhere( (f) => f.name == j['framework'], orElse: () => WebFramework.react, ), remoteDir: (j['remoteDir'] ?? '/opt/app') as String, remoteJarName: (j['remoteJarName'] ?? '') as String, serviceName: (j['serviceName'] ?? '') as String, nginxService: (j['nginxService'] ?? 'nginx') as String, logCommand: (j['logCommand'] ?? '') as String, localProjectPath: (j['localProjectPath'] ?? '') as String, buildCommand: (j['buildCommand'] ?? '') as String, jarPattern: (j['jarPattern'] ?? '') as String, outputSubdir: (j['outputSubdir'] ?? '') as String, healthType: HealthCheckType.values.firstWhere( (h) => h.name == j['healthType'], orElse: () => HealthCheckType.auto, ), healthTarget: (j['healthTarget'] ?? '') as String, ); }