feat(cluster): Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades
This commit is contained in:
		
							
								
								
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,16 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-09-05 - 5.1.0 - feat(cluster) | ||||
| Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades | ||||
|  | ||||
| - Introduce optional setupMode on cluster configs and requests (ICluster.data.setupMode, createCluster request) to allow 'manual' | 'hetzner' | 'aws' | 'digitalocean'. | ||||
| - ClusterManager: default setupMode to 'manual' when creating clusters and only trigger serverManager.ensureServerInfrastructure() for 'hetzner' clusters. | ||||
| - ServerManager: skip provisioning for clusters not configured with setupMode 'hetzner' and log skipped clusters. | ||||
| - Web UI: add a 'Setup Mode' dropdown when creating a cluster so users can choose auto-provisioning provider; ensure the add-cluster action passes setupMode. | ||||
| - Web UI: dashboard enhancements — add icons to view tabs and replace cluster overview with a stats grid (including total clusters, total servers, images, services, deployments, secret groups/bundles, DNS, DBs, backups, mails, s3). The overview now computes total servers across clusters. | ||||
| - Package dependency bumps (devDependencies and dependencies) to keep libs up-to-date (examples: @git.zone/tsbuild, @git.zone/tstest, @api.global/typedserver, @apiclient.xyz/docker, @design.estate/dees-catalog, @push.rocks/smartlog, @push.rocks/smartrequest, @push.rocks/taskbuffer, etc.). | ||||
| - Add .claude/settings.local.json with local Claude permissions (editor/automation config). | ||||
|  | ||||
| ## 2025-08-18 - 5.0.6 - fix(connector.letsencrypt) | ||||
| Improve Let's Encrypt integration and certificate handling; fix coreflow certificate response; add local assistant permissions config | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								package.json
									
									
									
									
									
								
							| @@ -22,24 +22,24 @@ | ||||
|     "docs": "tsdoc aidoc" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@git.zone/tsbuild": "^2.6.7", | ||||
|     "@git.zone/tsbuild": "^2.6.8", | ||||
|     "@git.zone/tsbundle": "^2.5.1", | ||||
|     "@git.zone/tsdoc": "^1.5.1", | ||||
|     "@git.zone/tspublish": "^1.10.3", | ||||
|     "@git.zone/tstest": "^2.3.5", | ||||
|     "@git.zone/tstest": "^2.3.6", | ||||
|     "@git.zone/tswatch": "^2.2.1", | ||||
|     "@types/node": "^22.0.0" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@api.global/typedrequest": "3.1.10", | ||||
|     "@api.global/typedrequest-interfaces": "^3.0.19", | ||||
|     "@api.global/typedserver": "^3.0.77", | ||||
|     "@api.global/typedserver": "^3.0.79", | ||||
|     "@api.global/typedsocket": "^3.0.1", | ||||
|     "@apiclient.xyz/cloudflare": "^6.4.1", | ||||
|     "@apiclient.xyz/docker": "^1.3.0", | ||||
|     "@apiclient.xyz/docker": "^1.3.5", | ||||
|     "@apiclient.xyz/hetznercloud": "^1.2.0", | ||||
|     "@apiclient.xyz/slack": "^3.0.9", | ||||
|     "@design.estate/dees-catalog": "^1.10.10", | ||||
|     "@design.estate/dees-catalog": "^1.10.12", | ||||
|     "@design.estate/dees-domtools": "^2.3.3", | ||||
|     "@design.estate/dees-element": "^2.1.2", | ||||
|     "@git.zone/tsrun": "^1.3.3", | ||||
| @@ -59,19 +59,19 @@ | ||||
|     "@push.rocks/smartguard": "^3.1.0", | ||||
|     "@push.rocks/smartjson": "^5.0.19", | ||||
|     "@push.rocks/smartjwt": "^2.2.1", | ||||
|     "@push.rocks/smartlog": "^3.1.8", | ||||
|     "@push.rocks/smartlog": "^3.1.9", | ||||
|     "@push.rocks/smartlog-destination-clickhouse": "^1.0.13", | ||||
|     "@push.rocks/smartlog-interfaces": "^3.0.2", | ||||
|     "@push.rocks/smartpath": "^6.0.0", | ||||
|     "@push.rocks/smartpromise": "^4.2.3", | ||||
|     "@push.rocks/smartrequest": "^4.2.2", | ||||
|     "@push.rocks/smartrequest": "^4.3.1", | ||||
|     "@push.rocks/smartrx": "^3.0.10", | ||||
|     "@push.rocks/smartssh": "^2.0.1", | ||||
|     "@push.rocks/smartstate": "^2.0.26", | ||||
|     "@push.rocks/smartstream": "^3.2.5", | ||||
|     "@push.rocks/smartstring": "^4.0.15", | ||||
|     "@push.rocks/smartunique": "^3.0.9", | ||||
|     "@push.rocks/taskbuffer": "^3.0.2", | ||||
|     "@push.rocks/taskbuffer": "^3.1.10", | ||||
|     "@push.rocks/webjwt": "^1.0.9", | ||||
|     "@tsclass/tsclass": "^9.2.0" | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										491
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										491
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@serve.zone/cloudly', | ||||
|   version: '5.0.6', | ||||
|   version: '5.1.0', | ||||
|   description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' | ||||
| } | ||||
|   | ||||
| @@ -24,11 +24,13 @@ export class ClusterManager { | ||||
|     this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>( | ||||
|       new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => { | ||||
|         // TODO: guards | ||||
|         const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified | ||||
|         const cluster = await this.createCluster({ | ||||
|           id: plugins.smartunique.uniSimple('cluster'), | ||||
|           data: { | ||||
|             userId: null, // this is created by the createCluster method | ||||
|             name: dataArg.clusterName, | ||||
|             setupMode: setupMode, | ||||
|             acmeInfo: null, | ||||
|             cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`, | ||||
|             servers: [], | ||||
| @@ -36,7 +38,12 @@ export class ClusterManager { | ||||
|           }, | ||||
|         }); | ||||
|         console.log(await cluster.createSavableObject()); | ||||
|         this.cloudlyRef.serverManager.ensureServerInfrastructure(); | ||||
|          | ||||
|         // Only auto-provision servers if setupMode is 'hetzner' | ||||
|         if (setupMode === 'hetzner') { | ||||
|           this.cloudlyRef.serverManager.ensureServerInfrastructure(); | ||||
|         } | ||||
|          | ||||
|         return { | ||||
|           cluster: await cluster.createSavableObject(), | ||||
|         }; | ||||
|   | ||||
| @@ -55,6 +55,12 @@ export class CloudlyServerManager { | ||||
|     // get all clusters | ||||
|     const allClusters = await this.cloudlyRef.clusterManager.getAllClusters(); | ||||
|     for (const cluster of allClusters) { | ||||
|       // Skip clusters that are not set up for Hetzner auto-provisioning | ||||
|       if (cluster.data.setupMode !== 'hetzner') { | ||||
|         console.log(`Skipping server provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`); | ||||
|         continue; | ||||
|       } | ||||
|  | ||||
|       // get existing servers | ||||
|       const servers = await this.getServersByCluster(cluster); | ||||
|  | ||||
|   | ||||
| @@ -18,6 +18,11 @@ export interface ICluster { | ||||
|      */ | ||||
|     cloudlyUrl?: string; | ||||
|  | ||||
|     /** | ||||
|      * Cluster setup mode - manual by default, or auto-provision with cloud provider | ||||
|      */ | ||||
|     setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; | ||||
|  | ||||
|     /** | ||||
|      * what servers are expected to be part of the cluster | ||||
|      */ | ||||
|   | ||||
| @@ -41,6 +41,7 @@ export interface IRequest_CreateCluster extends plugins.typedrequestInterfaces.i | ||||
|   request: { | ||||
|     identity: userInterfaces.IIdentity; | ||||
|     clusterName: string; | ||||
|     setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; | ||||
|   }; | ||||
|   response: { | ||||
|     cluster: clusterInterfaces.ICluster; | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@serve.zone/cloudly', | ||||
|   version: '5.0.6', | ||||
|   version: '5.1.0', | ||||
|   description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' | ||||
| } | ||||
|   | ||||
| @@ -245,6 +245,7 @@ export const addClusterAction = dataState.createAction( | ||||
|     statePartArg, | ||||
|     payloadArg: { | ||||
|       clusterName: string; | ||||
|       setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; | ||||
|     } | ||||
|   ) => { | ||||
|     let currentState = statePartArg.getState(); | ||||
|   | ||||
| @@ -76,66 +76,82 @@ export class CloudlyDashboard extends DeesElement { | ||||
|             .viewTabs=${[ | ||||
|               { | ||||
|                 name: 'Overview', | ||||
|                 iconName: 'lucide:LayoutDashboard', | ||||
|                 element: CloudlyViewOverview, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'SecretGroups', | ||||
|                 iconName: 'lucide:ShieldCheck', | ||||
|                 element: CloudlyViewSecretGroups, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'SecretBundles', | ||||
|                 iconName: 'lucide:LockKeyhole', | ||||
|                 element: CloudlyViewSecretBundles, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Clusters', | ||||
|                 iconName: 'lucide:Network', | ||||
|                 element: CloudlyViewClusters, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'ExternalRegistries', | ||||
|                 iconName: 'lucide:Package', | ||||
|                 element: CloudlyViewExternalRegistries, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Images', | ||||
|                 iconName: 'lucide:Image', | ||||
|                 element: CloudlyViewImages, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Services', | ||||
|                 iconName: 'lucide:Layers', | ||||
|                 element: CloudlyViewServices, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Testing & Building', | ||||
|                 iconName: 'lucide:HardHat', | ||||
|                 element: CloudlyViewServices, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Deployments', | ||||
|                 iconName: 'lucide:Rocket', | ||||
|                 element: CloudlyViewDeployments, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'DNS', | ||||
|                 iconName: 'lucide:Globe', | ||||
|                 element: CloudlyViewDns, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Mails', | ||||
|                 iconName: 'lucide:Mail', | ||||
|                 element: CloudlyViewMails, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Logs', | ||||
|                 iconName: 'lucide:FileText', | ||||
|                 element: CloudlyViewLogs, | ||||
|               }, | ||||
|               { | ||||
|                 name: 's3', | ||||
|                 iconName: 'lucide:Cloud', | ||||
|                 element: CloudlyViewS3, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'DBs', | ||||
|                 iconName: 'lucide:Database', | ||||
|                 element: CloudlyViewDbs, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Backups', | ||||
|                 iconName: 'lucide:Save', | ||||
|                 element: CloudlyViewBackups, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Fleet', | ||||
|                 iconName: 'lucide:Truck', | ||||
|                 element: CloudlyViewBackups, | ||||
|               } | ||||
|             ] as plugins.deesCatalog.IView[]} | ||||
|   | ||||
| @@ -68,6 +68,18 @@ export class CloudlyViewClusters extends DeesElement { | ||||
|                       .description=${'a descriptive name for the cluster'} | ||||
|                       .value=${''} | ||||
|                     ></dees-input-text> | ||||
|                     <dees-input-dropdown | ||||
|                       .key=${'setupMode'} | ||||
|                       .label=${'Setup Mode'} | ||||
|                       .description=${'How the cluster infrastructure should be managed'} | ||||
|                       .options=${[ | ||||
|                         {option: 'manual', key: 'manual', description: 'Manual Setup - Add your own servers manually'}, | ||||
|                         {option: 'hetzner', key: 'hetzner', description: 'Hetzner Cloud - Auto-provision servers on Hetzner'}, | ||||
|                         {option: 'aws', key: 'aws', description: 'AWS - Auto-provision on Amazon Web Services (coming soon)', disabled: true}, | ||||
|                         {option: 'digitalocean', key: 'digitalocean', description: 'DigitalOcean - Auto-provision on DigitalOcean (coming soon)', disabled: true} | ||||
|                       ]} | ||||
|                       .selectedOption=${'manual'} | ||||
|                     ></dees-input-dropdown> | ||||
|                   </dees-form> | ||||
|                 `, | ||||
|                 menuOptions: [ | ||||
| @@ -76,6 +88,7 @@ export class CloudlyViewClusters extends DeesElement { | ||||
|                     action: async (modalArg) => { | ||||
|                       const data: { | ||||
|                         clusterName: string; | ||||
|                         setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean'; | ||||
|                       } = (await modalArg.shadowRoot | ||||
|                         .querySelector('dees-form') | ||||
|                         .collectFormData()) as any; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as shared from '../elements/shared/index.js'; | ||||
|  | ||||
| import { | ||||
| @@ -34,34 +33,124 @@ export class CloudlyViewOverview extends DeesElement { | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .clusterGrid { | ||||
|         display: grid; | ||||
|         grid-template-columns: ${cssManager.cssGridColumns(3, 8)}; | ||||
|         grid-gap: 16px; | ||||
|         margin-bottom: 40px; | ||||
|       dees-statsgrid { | ||||
|         margin-top: 24px; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   public render() { | ||||
|     // Calculate total servers across all clusters | ||||
|     const totalServers = this.data.clusters?.reduce((sum, cluster) =>  | ||||
|       sum + (cluster.data.servers?.length || 0), 0) || 0; | ||||
|  | ||||
|     // Create tiles for the stats grid | ||||
|     const statsTiles = [ | ||||
|       { | ||||
|         id: 'clusters', | ||||
|         title: 'Total Clusters', | ||||
|         value: this.data.clusters?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Network', | ||||
|         description: 'Active clusters' | ||||
|       }, | ||||
|       { | ||||
|         id: 'servers', | ||||
|         title: 'Total Servers', | ||||
|         value: totalServers, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Server', | ||||
|         description: 'Connected servers' | ||||
|       }, | ||||
|       { | ||||
|         id: 'services', | ||||
|         title: 'Services', | ||||
|         value: this.data.services?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Layers', | ||||
|         description: 'Deployed services' | ||||
|       }, | ||||
|       { | ||||
|         id: 'deployments', | ||||
|         title: 'Deployments', | ||||
|         value: this.data.deployments?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Rocket', | ||||
|         description: 'Active deployments' | ||||
|       }, | ||||
|       { | ||||
|         id: 'secretGroups', | ||||
|         title: 'Secret Groups', | ||||
|         value: this.data.secretGroups?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:ShieldCheck', | ||||
|         description: 'Configured secret groups' | ||||
|       }, | ||||
|       { | ||||
|         id: 'secretBundles', | ||||
|         title: 'Secret Bundles', | ||||
|         value: this.data.secretBundles?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:LockKeyhole', | ||||
|         description: 'Available secret bundles' | ||||
|       }, | ||||
|       { | ||||
|         id: 'images', | ||||
|         title: 'Images', | ||||
|         value: this.data.images?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Image', | ||||
|         description: 'Container images' | ||||
|       }, | ||||
|       { | ||||
|         id: 'dns', | ||||
|         title: 'DNS Zones', | ||||
|         value: this.data.dns?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Globe', | ||||
|         description: 'Managed DNS zones' | ||||
|       }, | ||||
|       { | ||||
|         id: 'databases', | ||||
|         title: 'Databases', | ||||
|         value: this.data.dbs?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Database', | ||||
|         description: 'Database instances' | ||||
|       }, | ||||
|       { | ||||
|         id: 'backups', | ||||
|         title: 'Backups', | ||||
|         value: this.data.backups?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Save', | ||||
|         description: 'Available backups' | ||||
|       }, | ||||
|       { | ||||
|         id: 'mails', | ||||
|         title: 'Mail Domains', | ||||
|         value: this.data.mails?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Mail', | ||||
|         description: 'Mail configurations' | ||||
|       }, | ||||
|       { | ||||
|         id: 's3', | ||||
|         title: 'S3 Buckets', | ||||
|         value: this.data.s3?.length || 0, | ||||
|         type: 'number' as const, | ||||
|         iconName: 'lucide:Cloud', | ||||
|         description: 'Storage buckets' | ||||
|       } | ||||
|     ]; | ||||
|  | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Overview</cloudly-sectionheading> | ||||
|       ${this.data.clusters.length === 0 ? html` | ||||
|         You need to create at least one cluster to see an overview. | ||||
|       `: html``} | ||||
|       ${this.data.clusters.map( | ||||
|         (clusterArg) => html` | ||||
|           <dees-label .label=${'cluster: ' + clusterArg.data.name}></dees-label> | ||||
|           <div class="clusterGrid"> | ||||
|             <dees-chart-area .label=${'System Usage'}></dees-chart-area> | ||||
|             <dees-chart-area .label=${'Internet Traffic'}></dees-chart-area> | ||||
|             <dees-chart-area .label=${'Requests'}></dees-chart-area> | ||||
|             <dees-chart-area .label=${'WebSocket Connections'}></dees-chart-area> | ||||
|             <dees-chart-log class="services" .label=${'Deployed Services'}></dees-chart-log> | ||||
|             <dees-chart-log class="eventLog" .label=${'Event Log'}></dees-chart-log> | ||||
|           </div> | ||||
|         ` | ||||
|       )} | ||||
|       <dees-statsgrid | ||||
|         .tiles=${statsTiles} | ||||
|         .minTileWidth=${250} | ||||
|         .gap=${16} | ||||
|       ></dees-statsgrid> | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user