Compare commits

..

20 Commits
v5.3.0 ... main

Author SHA1 Message Date
94e0c38191 feat(domain): improve domain update logic and ensure default activation state 2025-09-14 17:44:59 +00:00
6cc3700d29 feat(domains): enhance domain management with activation states and sync options 2025-09-14 17:38:16 +00:00
bb313fd9dc feat: Add settings view for cloud provider configurations
- Implemented CloudlyViewSettings component for managing cloud provider settings including Hetzner, Cloudflare, AWS, DigitalOcean, Azure, and Google Cloud.
- Added functionality to load, save, and test connections for each provider.
- Enhanced UI with loading states and success/error notifications.

feat: Create tasks view with execution history

- Developed CloudlyViewTasks component to display and manage tasks and their executions.
- Integrated auto-refresh functionality for task executions.
- Added filtering and searching capabilities for tasks.

feat: Implement execution details and task panel components

- Created CloudlyExecutionDetails component to show detailed information about task executions including logs and metrics.
- Developed CloudlyTaskPanel component to display individual tasks with execution status and actions to run or cancel tasks.

feat: Utility functions for formatting and categorization

- Added utility functions for formatting dates, durations, and cron expressions.
- Implemented functions to retrieve category icons and hues for task categorization.
2025-09-14 17:28:21 +00:00
5ef8621db7 feat(task-execution): implement task cancellation handling and improve UI feedback for canceling tasks 2025-09-12 23:53:10 +00:00
6cd348ca28 feat(cloudly-view-tasks): add search and category filters, implement auto-refresh for task executions 2025-09-12 23:38:18 +00:00
3183f9e909 chore(deps): update dependencies for @push.rocks/smartjson, @push.rocks/smartstate, @push.rocks/smartstring, and @git.zone/tstest 2025-09-12 22:20:30 +00:00
ff7004412b feat(appstate): Remove helper function for stripping class instances from data fetching 2025-09-12 10:58:16 +00:00
f07bcc4660 feat(appstate): Refactor data fetching to use helper for stripping class instances 2025-09-12 07:56:06 +00:00
d773e13aab feat(api-client): Add update and delete methods for external registries and secret bundles/groups 2025-09-10 20:33:32 +00:00
dc0d128892 feat(api-client): Add advanced cluster creation method and refactor login actions to use API client 2025-09-10 20:23:12 +00:00
124c4ca46f feat: Enhance API client integration across web and CLI
- Added typedRequestInterfaces import to plugins.ts for better type handling.
- Updated CLI client to utilize environment variables for Cloudly API credentials and improved authentication flow.
- Refactored appstate.ts to use a shared API client instance, reducing redundancy in API calls for various actions.
- Simplified external registry actions in appstate.ts by leveraging the shared API client.
- Updated CloudlyDashboard and CloudlyViewSettings components to utilize the shared API client for fetching settings and managing connections.
- Removed redundant TypedRequest instances in favor of direct API client calls for improved performance and maintainability.
- Exposed the API client in plugins.ts for easier access in UI components.
2025-09-10 19:06:16 +00:00
5d281d9b6c feat(tasks): Enhance task management with identity handling and initial data loading 2025-09-10 17:04:18 +00:00
5b37bb5b11 feat: Implement Cloudly Task Manager with predefined tasks and execution tracking
- Added CloudlyTaskManager class for managing tasks, including registration, execution, scheduling, and cancellation.
- Created predefined tasks: DNS Sync, Certificate Renewal, Cleanup, Health Check, Resource Report, Database Maintenance, Security Scan, and Docker Cleanup.
- Introduced ITaskExecution interface for tracking task execution details and outcomes.
- Developed API request interfaces for task management operations (getTasks, getTaskExecutions, triggerTask, cancelTask).
- Implemented CloudlyViewTasks web component for displaying tasks and their execution history, including filtering and detailed views.
2025-09-10 16:37:03 +00:00
fd1da01a3f feat(dns): Enhance DNS management with auto-generated entries and service activation 2025-09-10 15:38:42 +00:00
6a447369f8 feat(external-registry): Enhance authentication handling and update UI for external registries 2025-09-10 08:50:32 +00:00
01d877f7ed feat(external-registry): Implement CRUD operations and connection verification for external registries 2025-09-10 08:24:55 +00:00
73505d1ed8 feat(dns): Add domain validation and dropdown for DNS entry creation and updates 2025-09-09 15:13:44 +00:00
766191899c feat(dns): Implement DNS management functionality
- Added DnsManager and DnsEntry classes to handle DNS entries.
- Introduced new interfaces for DNS entry requests and data structures.
- Updated Cloudly class to include DnsManager instance.
- Enhanced app state to manage DNS entries and actions for creating, updating, and deleting DNS records.
- Created UI components for DNS management, including forms for adding and editing DNS entries.
- Updated overview and services views to reflect DNS entries.
- Added validation and formatting methods for DNS entries.
2025-09-09 15:08:28 +00:00
38e8b4086d feat(requests): Add deploymentRequests to index for improved request handling 2025-09-08 13:03:33 +00:00
ce047d1bb0 feat(deployment): Implement Deployment and DeploymentManager classes with CRUD operations and service integration 2025-09-08 12:46:23 +00:00
69 changed files with 6819 additions and 3553 deletions

View File

@@ -26,7 +26,7 @@
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsdoc": "^1.5.2", "@git.zone/tsdoc": "^1.5.2",
"@git.zone/tspublish": "^1.10.3", "@git.zone/tspublish": "^1.10.3",
"@git.zone/tstest": "^2.3.6", "@git.zone/tstest": "^2.3.8",
"@git.zone/tswatch": "^2.2.1", "@git.zone/tswatch": "^2.2.1",
"@types/node": "^22.0.0" "@types/node": "^22.0.0"
}, },
@@ -39,7 +39,7 @@
"@apiclient.xyz/docker": "^1.3.5", "@apiclient.xyz/docker": "^1.3.5",
"@apiclient.xyz/hetznercloud": "^1.2.0", "@apiclient.xyz/hetznercloud": "^1.2.0",
"@apiclient.xyz/slack": "^3.0.9", "@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.11.2", "@design.estate/dees-catalog": "^1.11.3",
"@design.estate/dees-domtools": "^2.3.3", "@design.estate/dees-domtools": "^2.3.3",
"@design.estate/dees-element": "^2.1.2", "@design.estate/dees-element": "^2.1.2",
"@git.zone/tsrun": "^1.3.3", "@git.zone/tsrun": "^1.3.3",
@@ -57,7 +57,7 @@
"@push.rocks/smartexpect": "^2.5.0", "@push.rocks/smartexpect": "^2.5.0",
"@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartguard": "^3.1.0", "@push.rocks/smartguard": "^3.1.0",
"@push.rocks/smartjson": "^5.0.19", "@push.rocks/smartjson": "^5.2.0",
"@push.rocks/smartjwt": "^2.2.1", "@push.rocks/smartjwt": "^2.2.1",
"@push.rocks/smartlog": "^3.1.9", "@push.rocks/smartlog": "^3.1.9",
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13", "@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
@@ -67,9 +67,9 @@
"@push.rocks/smartrequest": "^4.3.1", "@push.rocks/smartrequest": "^4.3.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartssh": "^2.0.1", "@push.rocks/smartssh": "^2.0.1",
"@push.rocks/smartstate": "^2.0.26", "@push.rocks/smartstate": "^2.0.27",
"@push.rocks/smartstream": "^3.2.5", "@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smartstring": "^4.0.15", "@push.rocks/smartstring": "^4.1.0",
"@push.rocks/smartunique": "^3.0.9", "@push.rocks/smartunique": "^3.0.9",
"@push.rocks/taskbuffer": "^3.4.0", "@push.rocks/taskbuffer": "^3.4.0",
"@push.rocks/webjwt": "^1.0.9", "@push.rocks/webjwt": "^1.0.9",

273
pnpm-lock.yaml generated
View File

@@ -33,8 +33,8 @@ importers:
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^1.11.2 specifier: ^1.11.3
version: 1.11.2(@tiptap/pm@2.26.1) version: 1.11.3(@tiptap/pm@2.26.1)
'@design.estate/dees-domtools': '@design.estate/dees-domtools':
specifier: ^2.3.3 specifier: ^2.3.3
version: 2.3.3 version: 2.3.3
@@ -87,8 +87,8 @@ importers:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0 version: 3.1.0
'@push.rocks/smartjson': '@push.rocks/smartjson':
specifier: ^5.0.19 specifier: ^5.2.0
version: 5.0.20 version: 5.2.0
'@push.rocks/smartjwt': '@push.rocks/smartjwt':
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1 version: 2.2.1
@@ -117,14 +117,14 @@ importers:
specifier: ^2.0.1 specifier: ^2.0.1
version: 2.0.1 version: 2.0.1
'@push.rocks/smartstate': '@push.rocks/smartstate':
specifier: ^2.0.26 specifier: ^2.0.27
version: 2.0.26 version: 2.0.27
'@push.rocks/smartstream': '@push.rocks/smartstream':
specifier: ^3.2.5 specifier: ^3.2.5
version: 3.2.5 version: 3.2.5
'@push.rocks/smartstring': '@push.rocks/smartstring':
specifier: ^4.0.15 specifier: ^4.1.0
version: 4.0.15 version: 4.1.0
'@push.rocks/smartunique': '@push.rocks/smartunique':
specifier: ^3.0.9 specifier: ^3.0.9
version: 3.0.9 version: 3.0.9
@@ -151,8 +151,8 @@ importers:
specifier: ^1.10.3 specifier: ^1.10.3
version: 1.10.3 version: 1.10.3
'@git.zone/tstest': '@git.zone/tstest':
specifier: ^2.3.6 specifier: ^2.3.8
version: 2.3.6(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2) version: 2.3.8(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)
'@git.zone/tswatch': '@git.zone/tswatch':
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1 version: 2.2.1
@@ -476,8 +476,8 @@ packages:
'@dabh/diagnostics@2.0.3': '@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@design.estate/dees-catalog@1.11.2': '@design.estate/dees-catalog@1.11.3':
resolution: {integrity: sha512-gMK+wDKXDBPzfWmaJySotjjp5A9rwk2PQANQF8V6Q52xUfKKUv7gHj4eju+pN6qkUA5OUzdCDplUeUCrA8i37w==} resolution: {integrity: sha512-gXGi6PlaHY4+lXHo17p+R/L6/QaqtN/3JFzTUXPl4J0fVKqVrEp22+lf7uvgAhs4WpV1Vd/c9yoyQ6JmrNSj4g==}
'@design.estate/dees-comms@1.0.27': '@design.estate/dees-comms@1.0.27':
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==} resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
@@ -846,8 +846,8 @@ packages:
resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==} resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==}
hasBin: true hasBin: true
'@git.zone/tstest@2.3.6': '@git.zone/tstest@2.3.8':
resolution: {integrity: sha512-2dcVM1WvQj9FoLPRWbLgBCWnDK0auI2c2vJxUzrLe0bi/ci50yrXxyKb2FIToQ+kOVe234Yb6jhNyp/d/zyHMQ==} resolution: {integrity: sha512-rt7rpR2UwzHXjpqquEvWG4LfzGOGeI6lcR2YyO8pc7lqjhH+xsuaWPUQ5IwFl4Vw4VnR9ZoHBCqkjvxF8ow1wQ==}
hasBin: true hasBin: true
'@git.zone/tswatch@2.2.1': '@git.zone/tswatch@2.2.1':
@@ -1200,68 +1200,68 @@ packages:
'@mongodb-js/saslprep@1.3.0': '@mongodb-js/saslprep@1.3.0':
resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==} resolution: {integrity: sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==}
'@napi-rs/canvas-android-arm64@0.1.78': '@napi-rs/canvas-android-arm64@0.1.79':
resolution: {integrity: sha512-N1ikxztjrRmh8xxlG5kYm1RuNr8ZW1EINEDQsLhhuy7t0pWI/e7SH91uFVLZKCMDyjel1tyWV93b5fdCAi7ggw==} resolution: {integrity: sha512-ih6ZIztNDEXl7axvC4swOwLFrM9lOyJa9VAMq7xIBtEZhR/8IVDa0ZTup2fZEiTCmnjmXolzv7uDviHkOTEMKQ==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@napi-rs/canvas-darwin-arm64@0.1.78': '@napi-rs/canvas-darwin-arm64@0.1.79':
resolution: {integrity: sha512-FA3aCU3G5yGc74BSmnLJTObnZRV+HW+JBTrsU+0WVVaNyVKlb5nMvYAQuieQlRVemsAA2ek2c6nYtHh6u6bwFw==} resolution: {integrity: sha512-REMz1Fac2VlOYJDg+JjmQWSJc459cCgVom6GvKwWkDqzSjvG9BSo72MDmQY3uhb7r49Xuz5gTFcLYTfNcm4MoA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-darwin-x64@0.1.78': '@napi-rs/canvas-darwin-x64@0.1.79':
resolution: {integrity: sha512-xVij69o9t/frixCDEoyWoVDKgE3ksLGdmE2nvBWVGmoLu94MWUlv2y4Qzf5oozBmydG5Dcm4pRHFBM7YWa1i6g==} resolution: {integrity: sha512-uQxLg6Bll7zv/ljp/YIeiUFWfV9C/ESv+2ioUh60hIAypuhtg6hhtWE/KnoW7G48wQls5VUStvEnJbnJ7bPKlA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.78': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.79':
resolution: {integrity: sha512-aSEXrLcIpBtXpOSnLhTg4jPsjJEnK7Je9KqUdAWjc7T8O4iYlxWxrXFIF8rV8J79h5jNdScgZpAUWYnEcutR3g==} resolution: {integrity: sha512-X37B//TVIipL/3RyvyfNlbQK2uyIaK3PJ2bH7ZeU+jpkaYprBsV15GCN/LHTYAi6R0F/c53zK3aSFNKkGHM/Og==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-gnu@0.1.78': '@napi-rs/canvas-linux-arm64-gnu@0.1.79':
resolution: {integrity: sha512-dlEPRX1hLGKaY3UtGa1dtkA1uGgFITn2mDnfI6YsLlYyLJQNqHx87D1YTACI4zFCUuLr/EzQDzuX+vnp9YveVg==} resolution: {integrity: sha512-+T1fuau1heabE6zGXiqZBGPH5fTIQF+xEu/u4fuugxEiChRYlhnPjkw26MBi8ePg/jmzxLfJEij6LMJQ4AQa2A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-arm64-musl@0.1.78': '@napi-rs/canvas-linux-arm64-musl@0.1.79':
resolution: {integrity: sha512-TsCfjOPZtm5Q/NO1EZHR5pwDPSPjPEttvnv44GL32Zn1uvudssjTLbvaG1jHq81Qxm16GTXEiYLmx4jOLZQYlg==} resolution: {integrity: sha512-KsrsR3+6uXv70W/1/kY0yRK4/bbdJgA1Vuxw4KyfSc6mjl1DMoYXDAjpBT/5w7AXy6cGG44jm3upvvt/y/dPfg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-riscv64-gnu@0.1.78': '@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
resolution: {integrity: sha512-+cpTTb0GDshEow/5Fy8TpNyzaPsYb3clQIjgWRmzRcuteLU+CHEU/vpYvAcSo7JxHYPJd8fjSr+qqh+nI5AtmA==} resolution: {integrity: sha512-EXaENnSJD6au6z4aKN2PpU9eVNWUsRI2cApm8gCa0WSRMaiYXZsFkXQmhB+Vz2pXahOS8BN2Zd8S1IeML/LCtg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-gnu@0.1.78': '@napi-rs/canvas-linux-x64-gnu@0.1.79':
resolution: {integrity: sha512-wxRcvKfvYBgtrO0Uy8OmwvjlnTcHpY45LLwkwVNIWHPqHAsyoTyG/JBSfJ0p5tWRzMOPDCDqdhpIO4LOgXjeyg==} resolution: {integrity: sha512-3xZhHlE9e3cd9D7Comy6/TTSs/8PUGXEXymIwYQrA1QxHojAlAOFlVai4rffzXd0bHylZu+/wD76LodvYqF1Yw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-linux-x64-musl@0.1.78': '@napi-rs/canvas-linux-x64-musl@0.1.79':
resolution: {integrity: sha512-vQFOGwC9QDP0kXlhb2LU1QRw/humXgcbVp8mXlyBqzc/a0eijlLF9wzyarHC1EywpymtS63TAj8PHZnhTYN6hg==} resolution: {integrity: sha512-4yv550uCjIEoTFgrpxYZK67nFlDMCQa3LAheM2QrO+B8w1p5w04usIQSCHqHe6aPWlbLQCIqfVcew6/7Q4KuHg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@napi-rs/canvas-win32-x64-msvc@0.1.78': '@napi-rs/canvas-win32-x64-msvc@0.1.79':
resolution: {integrity: sha512-/eKlTZBtGUgpRKalzOzRr6h7KVSuziESWXgBcBnXggZmimwIJWPJlEcbrx5Tcwj8rPuZiANXQOG9pPgy9Q4LTQ==} resolution: {integrity: sha512-sD5qP2njBRnhNlTNFJDdpeCN6aR3qVamLySTwhX3ec8sdfeT/chf/x2dw2UXoIGMoVaVk/y2ifwxBj/h2a2jug==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@napi-rs/canvas@0.1.78': '@napi-rs/canvas@0.1.79':
resolution: {integrity: sha512-YaBHJvT+T1DoP16puvWM6w46Lq3VhwKIJ8th5m1iEJyGh7mibk5dT7flBvMQ1EH1LYmMzXJ+OUhu+8wQ9I6u7g==} resolution: {integrity: sha512-0SkvRRjyxY35eniEsQsjPYUMWunKlAWvionJOzJJADZF5ZDf/sL+ncJbMTV5LUiHg1iHOvVjWcuDOx/GNXr/lA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
'@napi-rs/wasm-runtime@1.0.3': '@napi-rs/wasm-runtime@1.0.3':
@@ -1440,6 +1440,9 @@ packages:
'@push.rocks/smartdns@6.2.2': '@push.rocks/smartdns@6.2.2':
resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==} resolution: {integrity: sha512-MhJcHujbyIuwIIFdnXb2OScGtRjNsliLUS8GoAurFsKtcCOaA0ytfP+PNzkukyBufjb1nMiJF3rjhswXdHakAQ==}
'@push.rocks/smartdns@7.6.1':
resolution: {integrity: sha512-nnP5+A2GOt0WsHrYhtKERmjdEHUchc+QbCCBEqlyeQTn+mNfx2WZvKVI1DFRJt8lamvzxP6Hr/BSe3WHdh4Snw==}
'@push.rocks/smartenv@5.0.12': '@push.rocks/smartenv@5.0.12':
resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==} resolution: {integrity: sha512-tDEFwywzq0FNzRYc9qY2dRl2pgQuZG0G2/yml2RLWZWSW+Fn1EHshnKOGHz8o77W7zvu4hTgQQX42r/JY5XHTg==}
@@ -1479,6 +1482,9 @@ packages:
'@push.rocks/smarthash@3.2.3': '@push.rocks/smarthash@3.2.3':
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==} resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
'@push.rocks/smarthash@3.2.6':
resolution: {integrity: sha512-Mq/WNX0Tjjes3X1gHd/ZBwOOKSrAG/Z3Xoc0OcCm3P20WKpniihkMpsnlE7wGjvpHLi/ZRe/XkB3KC3d5r9X4g==}
'@push.rocks/smarti18n@1.0.4': '@push.rocks/smarti18n@1.0.4':
resolution: {integrity: sha512-bHIi9Iuzp2cbux9q79ZK5jOQYPsYJ9zDDS4p/xEPQH31gr0mcFRosLSQb1kvDQDVmUhI0ADlQMqr2ui9zEXQHA==} resolution: {integrity: sha512-bHIi9Iuzp2cbux9q79ZK5jOQYPsYJ9zDDS4p/xEPQH31gr0mcFRosLSQb1kvDQDVmUhI0ADlQMqr2ui9zEXQHA==}
@@ -1488,8 +1494,8 @@ packages:
'@push.rocks/smartjimp@1.2.0': '@push.rocks/smartjimp@1.2.0':
resolution: {integrity: sha512-SPz8p2ZuphNqIXK/UDsNFrnpJn/jr6FbuBSMQc0V2v2ffQIF32ZqktKQpXpitiqD1K5JEYS56JAhlYHgrAu7yw==} resolution: {integrity: sha512-SPz8p2ZuphNqIXK/UDsNFrnpJn/jr6FbuBSMQc0V2v2ffQIF32ZqktKQpXpitiqD1K5JEYS56JAhlYHgrAu7yw==}
'@push.rocks/smartjson@5.0.20': '@push.rocks/smartjson@5.2.0':
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==} resolution: {integrity: sha512-710e8UwovRfPgUtaBHcd6unaODUjV5fjxtGcGCqtaTcmvOV6VpasdVfT66xMDzQmWH2E9ZfHDJeso9HdDQzNQA==}
'@push.rocks/smartjwt@2.2.1': '@push.rocks/smartjwt@2.2.1':
resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==} resolution: {integrity: sha512-Xwau9o8u7kLfSGi5v+kiyGB/hiDPclZjVEuj69J0LszO9nOh4OexYizKIOgOzKQMqnYQ03Dy35KqP9pdEjccbQ==}
@@ -1530,6 +1536,9 @@ packages:
'@push.rocks/smartnetwork@4.1.2': '@push.rocks/smartnetwork@4.1.2':
resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==} resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==}
'@push.rocks/smartnetwork@4.4.0':
resolution: {integrity: sha512-OvFtz41cvQ7lcXwaIOhghNUUlNoMxvwKDctbDvMyuZyEH08SpLjhyv2FuKbKL/mgwA/WxakTbohoC8SW7t+kiw==}
'@push.rocks/smartnpm@2.0.6': '@push.rocks/smartnpm@2.0.6':
resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==} resolution: {integrity: sha512-7anKDOjX6gXWs1IAc+YWz9ZZ8gDsTwaLh+CxRnGHjAawOmK788NrrgVCg2Fb3qojrPnoxecc46F8Ivp1BT7Izw==}
@@ -1599,8 +1608,8 @@ packages:
'@push.rocks/smartssh@2.0.1': '@push.rocks/smartssh@2.0.1':
resolution: {integrity: sha512-S+NFu1PYjsuExTUTQybXsT4r+mjqKydpAOfFvosF5ATO0EBD+nJc1TVx49dzAPO4/gs71Bxe3eLZWwZFwdKfsg==} resolution: {integrity: sha512-S+NFu1PYjsuExTUTQybXsT4r+mjqKydpAOfFvosF5ATO0EBD+nJc1TVx49dzAPO4/gs71Bxe3eLZWwZFwdKfsg==}
'@push.rocks/smartstate@2.0.26': '@push.rocks/smartstate@2.0.27':
resolution: {integrity: sha512-lMcf0ZWWs9jej9wjapuonuIZiQNiD9NcAcvRDFXq7GtQf/HUyr6zr5K1XxGZaCIGyYrbYnBHBpNU+8DBoarHrA==} resolution: {integrity: sha512-q4UKir7GV3hakJWXQR4DoA4tUVwT5GRkJ/MtanHYF0wZLHfS19+nGmyO9y974zk3eT9hmy3+Lq5cKtU2W6+Y3w==}
'@push.rocks/smartstream@2.0.8': '@push.rocks/smartstream@2.0.8':
resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==} resolution: {integrity: sha512-GlF/9cCkvBHwKa3DK4DO5wjfSgqkj6gAS4TrY9uD5NMHu9RQv4WiNrElTYj7iCEpnZgUnLO3tzw1JA3NRIMnnA==}
@@ -1608,8 +1617,8 @@ packages:
'@push.rocks/smartstream@3.2.5': '@push.rocks/smartstream@3.2.5':
resolution: {integrity: sha512-PLGGIFDy8JLNVUnnntMSIYN4W081YSbNC7Y/sWpvUT8PAXtbEXXUiDFgK5o3gcI0ptpKQxHAwxhzNlPj0sbFVg==} resolution: {integrity: sha512-PLGGIFDy8JLNVUnnntMSIYN4W081YSbNC7Y/sWpvUT8PAXtbEXXUiDFgK5o3gcI0ptpKQxHAwxhzNlPj0sbFVg==}
'@push.rocks/smartstring@4.0.15': '@push.rocks/smartstring@4.1.0':
resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==} resolution: {integrity: sha512-Q4py/Nm3KTDhQ9EiC75yBtSTLR0KLMwhKM+8gGcutgKotZT6wJ3gncjmtD8LKFfNhb4lSaFMgPJgLrCHTOH6Iw==}
'@push.rocks/smarttime@4.0.8': '@push.rocks/smarttime@4.0.8':
resolution: {integrity: sha512-He+1ebBowVd8rW+VHZMFmz407xVMQf/JbyKr3s1ozoIlJS1AhZpDvlkzyqLV2tNMP1/cEBeo25ImJN2x1pksBA==} resolution: {integrity: sha512-He+1ebBowVd8rW+VHZMFmz407xVMQf/JbyKr3s1ozoIlJS1AhZpDvlkzyqLV2tNMP1/cEBeo25ImJN2x1pksBA==}
@@ -5271,6 +5280,12 @@ packages:
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true hasBin: true
systeminformation@5.27.8:
resolution: {integrity: sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==}
engines: {node: '>=8.0.0'}
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
tar-fs@3.1.0: tar-fs@3.1.0:
resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==}
@@ -5718,7 +5733,7 @@ snapshots:
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartfeed': 1.0.11 '@push.rocks/smartfeed': 1.0.11
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog': 3.1.9
'@push.rocks/smartlog-destination-devtools': 1.0.12 '@push.rocks/smartlog-destination-devtools': 1.0.12
'@push.rocks/smartlog-interfaces': 3.0.2 '@push.rocks/smartlog-interfaces': 3.0.2
@@ -5757,10 +5772,10 @@ snapshots:
'@api.global/typedrequest': 3.1.10 '@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/isohash': 2.0.1 '@push.rocks/isohash': 2.0.1
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartsocket': 2.0.27 '@push.rocks/smartsocket': 2.0.27
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0 '@push.rocks/smarturl': 3.1.0
transitivePeerDependencies: transitivePeerDependencies:
- '@nuxt/kit' - '@nuxt/kit'
@@ -5776,7 +5791,7 @@ snapshots:
'@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog': 3.1.9
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@tsclass/tsclass': 9.2.0 '@tsclass/tsclass': 9.2.0
cloudflare: 4.5.0 cloudflare: 4.5.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -5788,14 +5803,14 @@ snapshots:
'@push.rocks/smartarchive': 4.2.2 '@push.rocks/smartarchive': 4.2.2
'@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartbucket': 3.3.10
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog': 3.1.9
'@push.rocks/smartnetwork': 4.1.2 '@push.rocks/smartnetwork': 4.1.2
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 4.3.1 '@push.rocks/smartrequest': 4.3.1
'@push.rocks/smartstream': 3.2.5 '@push.rocks/smartstream': 3.2.5
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/smartversion': 3.0.5 '@push.rocks/smartversion': 3.0.5
'@tsclass/tsclass': 9.2.0 '@tsclass/tsclass': 9.2.0
@@ -6684,7 +6699,7 @@ snapshots:
enabled: 2.0.0 enabled: 2.0.0
kuler: 2.0.0 kuler: 2.0.0
'@design.estate/dees-catalog@1.11.2(@tiptap/pm@2.26.1)': '@design.estate/dees-catalog@1.11.3(@tiptap/pm@2.26.1)':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.3 '@design.estate/dees-domtools': 2.3.3
'@design.estate/dees-element': 2.1.2 '@design.estate/dees-element': 2.1.2
@@ -6695,7 +6710,7 @@ snapshots:
'@fortawesome/free-solid-svg-icons': 7.0.1 '@fortawesome/free-solid-svg-icons': 7.0.1
'@push.rocks/smarti18n': 1.0.4 '@push.rocks/smarti18n': 1.0.4
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@tiptap/core': 2.26.1(@tiptap/pm@2.26.1) '@tiptap/core': 2.26.1(@tiptap/pm@2.26.1)
'@tiptap/extension-link': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1) '@tiptap/extension-link': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))(@tiptap/pm@2.26.1)
'@tiptap/extension-text-align': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1)) '@tiptap/extension-text-align': 2.26.1(@tiptap/core@2.26.1(@tiptap/pm@2.26.1))
@@ -6732,13 +6747,13 @@ snapshots:
'@design.estate/dees-comms': 1.0.27 '@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartmarkdown': 3.0.3 '@push.rocks/smartmarkdown': 3.0.3
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrouter': 1.3.3 '@push.rocks/smartrouter': 1.3.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstate': 2.0.26 '@push.rocks/smartstate': 2.0.27
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarturl': 3.1.0 '@push.rocks/smarturl': 3.1.0
'@push.rocks/webrequest': 3.0.37 '@push.rocks/webrequest': 3.0.37
'@push.rocks/websetup': 3.0.19 '@push.rocks/websetup': 3.0.19
@@ -7068,7 +7083,7 @@ snapshots:
'@push.rocks/smartshell': 3.0.6 '@push.rocks/smartshell': 3.0.6
tsx: 4.19.2 tsx: 4.19.2
'@git.zone/tstest@2.3.6(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)': '@git.zone/tstest@2.3.8(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)(typescript@5.9.2)':
dependencies: dependencies:
'@api.global/typedserver': 3.0.79 '@api.global/typedserver': 3.0.79
'@git.zone/tsbundle': 2.5.1 '@git.zone/tsbundle': 2.5.1
@@ -7082,9 +7097,10 @@ snapshots:
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartexpect': 2.5.0 '@push.rocks/smartexpect': 2.5.0
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog': 3.1.9
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7) '@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)
'@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 4.3.1 '@push.rocks/smartrequest': 4.3.1
@@ -7578,48 +7594,48 @@ snapshots:
dependencies: dependencies:
sparse-bitfield: 3.0.3 sparse-bitfield: 3.0.3
'@napi-rs/canvas-android-arm64@0.1.78': '@napi-rs/canvas-android-arm64@0.1.79':
optional: true optional: true
'@napi-rs/canvas-darwin-arm64@0.1.78': '@napi-rs/canvas-darwin-arm64@0.1.79':
optional: true optional: true
'@napi-rs/canvas-darwin-x64@0.1.78': '@napi-rs/canvas-darwin-x64@0.1.79':
optional: true optional: true
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.78': '@napi-rs/canvas-linux-arm-gnueabihf@0.1.79':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-gnu@0.1.78': '@napi-rs/canvas-linux-arm64-gnu@0.1.79':
optional: true optional: true
'@napi-rs/canvas-linux-arm64-musl@0.1.78': '@napi-rs/canvas-linux-arm64-musl@0.1.79':
optional: true optional: true
'@napi-rs/canvas-linux-riscv64-gnu@0.1.78': '@napi-rs/canvas-linux-riscv64-gnu@0.1.79':
optional: true optional: true
'@napi-rs/canvas-linux-x64-gnu@0.1.78': '@napi-rs/canvas-linux-x64-gnu@0.1.79':
optional: true optional: true
'@napi-rs/canvas-linux-x64-musl@0.1.78': '@napi-rs/canvas-linux-x64-musl@0.1.79':
optional: true optional: true
'@napi-rs/canvas-win32-x64-msvc@0.1.78': '@napi-rs/canvas-win32-x64-msvc@0.1.79':
optional: true optional: true
'@napi-rs/canvas@0.1.78': '@napi-rs/canvas@0.1.79':
optionalDependencies: optionalDependencies:
'@napi-rs/canvas-android-arm64': 0.1.78 '@napi-rs/canvas-android-arm64': 0.1.79
'@napi-rs/canvas-darwin-arm64': 0.1.78 '@napi-rs/canvas-darwin-arm64': 0.1.79
'@napi-rs/canvas-darwin-x64': 0.1.78 '@napi-rs/canvas-darwin-x64': 0.1.79
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.78 '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.79
'@napi-rs/canvas-linux-arm64-gnu': 0.1.78 '@napi-rs/canvas-linux-arm64-gnu': 0.1.79
'@napi-rs/canvas-linux-arm64-musl': 0.1.78 '@napi-rs/canvas-linux-arm64-musl': 0.1.79
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.78 '@napi-rs/canvas-linux-riscv64-gnu': 0.1.79
'@napi-rs/canvas-linux-x64-gnu': 0.1.78 '@napi-rs/canvas-linux-x64-gnu': 0.1.79
'@napi-rs/canvas-linux-x64-musl': 0.1.78 '@napi-rs/canvas-linux-x64-musl': 0.1.79
'@napi-rs/canvas-win32-x64-msvc': 0.1.78 '@napi-rs/canvas-win32-x64-msvc': 0.1.79
optional: true optional: true
'@napi-rs/wasm-runtime@1.0.3': '@napi-rs/wasm-runtime@1.0.3':
@@ -7804,10 +7820,10 @@ snapshots:
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartexit': 1.0.23 '@push.rocks/smartexit': 1.0.23
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpath': 5.1.0 '@push.rocks/smartpath': 5.1.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.4.0 '@push.rocks/taskbuffer': 3.4.0
'@tsclass/tsclass': 4.4.4 '@tsclass/tsclass': 4.4.4
@@ -7826,10 +7842,10 @@ snapshots:
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartexit': 1.0.23 '@push.rocks/smartexit': 1.0.23
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.4.0 '@push.rocks/taskbuffer': 3.4.0
'@tsclass/tsclass': 9.2.0 '@tsclass/tsclass': 9.2.0
@@ -7878,7 +7894,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/qenv': 6.1.3 '@push.rocks/qenv': 6.1.3
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog': 3.1.9
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -7896,7 +7912,7 @@ snapshots:
'@push.rocks/smartfile': 10.0.41 '@push.rocks/smartfile': 10.0.41
'@push.rocks/smartpath': 5.1.0 '@push.rocks/smartpath': 5.1.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/qenv@6.1.3': '@push.rocks/qenv@6.1.3':
dependencies: dependencies:
@@ -7919,7 +7935,7 @@ snapshots:
'@push.rocks/smartnetwork': 4.1.2 '@push.rocks/smartnetwork': 4.1.2
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0 '@push.rocks/smartrequest': 2.1.0
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 9.2.0 '@tsclass/tsclass': 9.2.0
@@ -7928,7 +7944,6 @@ snapshots:
- '@aws-sdk/credential-providers' - '@aws-sdk/credential-providers'
- '@mongodb-js/zstd' - '@mongodb-js/zstd'
- '@nuxt/kit' - '@nuxt/kit'
- aws-crt
- encoding - encoding
- gcp-metadata - gcp-metadata
- kerberos - kerberos
@@ -8005,7 +8020,7 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstream': 3.2.5 '@push.rocks/smartstream': 3.2.5
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@tsclass/tsclass': 9.2.0 '@tsclass/tsclass': 9.2.0
transitivePeerDependencies: transitivePeerDependencies:
@@ -8075,7 +8090,7 @@ snapshots:
'@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7) '@push.rocks/smartmongo': 2.0.12(@aws-sdk/credential-providers@3.796.0)(socks@2.8.7)
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.4.0 '@push.rocks/taskbuffer': 3.4.0
@@ -8115,6 +8130,22 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@push.rocks/smartdns@7.6.1':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrequest': 2.1.0
'@tsclass/tsclass': 9.2.0
'@types/dns-packet': 5.6.5
'@types/elliptic': 6.4.18
acme-client: 5.4.0
dns-packet: 5.6.1
elliptic: 6.6.1
minimatch: 10.0.3
transitivePeerDependencies:
- supports-color
'@push.rocks/smartenv@5.0.12': '@push.rocks/smartenv@5.0.12':
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -8155,7 +8186,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile-interfaces': 1.0.7 '@push.rocks/smartfile-interfaces': 1.0.7
'@push.rocks/smarthash': 3.0.4 '@push.rocks/smarthash': 3.0.4
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartmime': 1.0.6 '@push.rocks/smartmime': 1.0.6
'@push.rocks/smartpath': 5.1.0 '@push.rocks/smartpath': 5.1.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -8174,7 +8205,7 @@ snapshots:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile-interfaces': 1.0.7 '@push.rocks/smartfile-interfaces': 1.0.7
'@push.rocks/smarthash': 3.2.3 '@push.rocks/smarthash': 3.2.3
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartmime': 2.0.4 '@push.rocks/smartmime': 2.0.4
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
@@ -8193,7 +8224,7 @@ snapshots:
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.3.0 '@push.rocks/smartshell': 3.3.0
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/smarttime': 4.1.1 '@push.rocks/smarttime': 4.1.1
'@types/diff': 8.0.0 '@types/diff': 8.0.0
diff: 8.0.2 diff: 8.0.2
@@ -8206,7 +8237,7 @@ snapshots:
'@push.rocks/smarthash@3.0.4': '@push.rocks/smarthash@3.0.4':
dependencies: dependencies:
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@types/through2': 2.0.41 '@types/through2': 2.0.41
through2: 4.0.2 through2: 4.0.2
@@ -8214,7 +8245,15 @@ snapshots:
'@push.rocks/smarthash@3.2.3': '@push.rocks/smarthash@3.2.3':
dependencies: dependencies:
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3
'@types/through2': 2.0.41
through2: 4.0.2
'@push.rocks/smarthash@3.2.6':
dependencies:
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@types/through2': 2.0.41 '@types/through2': 2.0.41
through2: 4.0.2 through2: 4.0.2
@@ -8244,10 +8283,10 @@ snapshots:
- supports-color - supports-color
- vue - vue
'@push.rocks/smartjson@5.0.20': '@push.rocks/smartjson@5.2.0':
dependencies: dependencies:
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
fast-json-stable-stringify: 2.1.0 fast-json-stable-stringify: 2.1.0
lodash.clonedeep: 4.5.0 lodash.clonedeep: 4.5.0
@@ -8255,7 +8294,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartcrypto': 2.0.4
'@push.rocks/smartguard': 3.1.0 '@push.rocks/smartguard': 3.1.0
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@tsclass/tsclass': 4.4.4 '@tsclass/tsclass': 4.4.4
'@types/jsonwebtoken': 9.0.7 '@types/jsonwebtoken': 9.0.7
jsonwebtoken: 9.0.2 jsonwebtoken: 9.0.2
@@ -8350,12 +8389,23 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartping': 1.0.8 '@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@types/default-gateway': 7.2.2 '@types/default-gateway': 7.2.2
isopen: 1.3.0 isopen: 1.3.0
public-ip: 7.0.1 public-ip: 7.0.1
systeminformation: 5.27.7 systeminformation: 5.27.7
'@push.rocks/smartnetwork@4.4.0':
dependencies:
'@push.rocks/smartdns': 7.6.1
'@push.rocks/smartping': 1.0.8
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.1.0
isopen: 1.3.0
systeminformation: 5.27.8
transitivePeerDependencies:
- supports-color
'@push.rocks/smartnpm@2.0.6': '@push.rocks/smartnpm@2.0.6':
dependencies: dependencies:
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.3
@@ -8405,7 +8455,7 @@ snapshots:
'@push.rocks/smartbuffer': 3.0.5 '@push.rocks/smartbuffer': 3.0.5
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartnetwork': 4.1.2 '@push.rocks/smartnetwork': 4.4.0
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2) '@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.2)
@@ -8553,7 +8603,7 @@ snapshots:
'@push.rocks/lik': 6.1.0 '@push.rocks/lik': 6.1.0
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.12
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartlog': 3.1.9 '@push.rocks/smartlog': 3.1.9
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -8584,16 +8634,16 @@ snapshots:
'@push.rocks/smartpath': 5.1.0 '@push.rocks/smartpath': 5.1.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartshell': 3.0.6 '@push.rocks/smartshell': 3.0.6
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@types/fs-extra': 11.0.4 '@types/fs-extra': 11.0.4
fs-extra: 11.2.0 fs-extra: 11.2.0
minimatch: 9.0.5 minimatch: 9.0.5
'@push.rocks/smartstate@2.0.26': '@push.rocks/smartstate@2.0.27':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smarthash': 3.2.3 '@push.rocks/smarthash': 3.2.6
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.20
@@ -8614,16 +8664,9 @@ snapshots:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@push.rocks/smartstring@4.0.15': '@push.rocks/smartstring@4.1.0':
dependencies: dependencies:
'@push.rocks/isounique': 1.0.5 '@push.rocks/isounique': 1.0.5
'@push.rocks/smartenv': 5.0.12
'@types/randomatic': 3.1.5
crypto-random-string: 5.0.0
js-base64: 3.7.7
randomatic: 3.1.1
strip-indent: 4.0.0
url: 0.11.4
'@push.rocks/smarttime@4.0.8': '@push.rocks/smarttime@4.0.8':
dependencies: dependencies:
@@ -8687,13 +8730,13 @@ snapshots:
'@push.rocks/webjwt@1.0.9': '@push.rocks/webjwt@1.0.9':
dependencies: dependencies:
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.1.0
'@push.rocks/webrequest@3.0.37': '@push.rocks/webrequest@3.0.37':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.12
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/webstore': 2.0.20 '@push.rocks/webstore': 2.0.20
@@ -8708,7 +8751,7 @@ snapshots:
'@api.global/typedrequest-interfaces': 3.0.19 '@api.global/typedrequest-interfaces': 3.0.19
'@push.rocks/lik': 6.1.0 '@push.rocks/lik': 6.1.0
'@push.rocks/smartenv': 5.0.12 '@push.rocks/smartenv': 5.0.12
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
'@tempfix/idb': 8.0.3 '@tempfix/idb': 8.0.3
@@ -12345,7 +12388,7 @@ snapshots:
pdfjs-dist@4.10.38: pdfjs-dist@4.10.38:
optionalDependencies: optionalDependencies:
'@napi-rs/canvas': 0.1.78 '@napi-rs/canvas': 0.1.79
peek-readable@4.1.0: {} peek-readable@4.1.0: {}
@@ -13114,6 +13157,8 @@ snapshots:
systeminformation@5.27.7: {} systeminformation@5.27.7: {}
systeminformation@5.27.8: {}
tar-fs@3.1.0: tar-fs@3.1.0:
dependencies: dependencies:
pump: 3.0.3 pump: 3.0.3

View File

@@ -16,13 +16,17 @@ import { MongodbConnector } from './connector.mongodb/connector.js';
// processes // processes
import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js'; import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js';
import { ClusterManager } from './manager.cluster/classes.clustermanager.js'; import { ClusterManager } from './manager.cluster/classes.clustermanager.js';
import { CloudlyTaskmanager } from './manager.task/taskmanager.js'; import { CloudlyTaskManager } from './manager.task/classes.taskmanager.js';
import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'; import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js';
import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js'; import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js';
import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js'; import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js';
import { ExternalApiManager } from './manager.status/statusmanager.js'; import { ExternalApiManager } from './manager.status/statusmanager.js';
import { ExternalRegistryManager } from './manager.externalregistry/index.js'; import { ExternalRegistryManager } from './manager.externalregistry/index.js';
import { ImageManager } from './manager.image/classes.imagemanager.js'; import { ImageManager } from './manager.image/classes.imagemanager.js';
import { ServiceManager } from './manager.service/classes.servicemanager.js';
import { DeploymentManager } from './manager.deployment/classes.deploymentmanager.js';
import { DnsManager } from './manager.dns/classes.dnsmanager.js';
import { DomainManager } from './manager.domain/classes.domainmanager.js';
import { logger } from './logger.js'; import { logger } from './logger.js';
import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js'; import { CloudlyAuthManager } from './manager.auth/classes.authmanager.js';
import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js'; import { CloudlySettingsManager } from './manager.settings/classes.settingsmanager.js';
@@ -60,7 +64,11 @@ export class Cloudly {
public externalApiManager: ExternalApiManager; public externalApiManager: ExternalApiManager;
public externalRegistryManager: ExternalRegistryManager; public externalRegistryManager: ExternalRegistryManager;
public imageManager: ImageManager; public imageManager: ImageManager;
public taskManager: CloudlyTaskmanager; public serviceManager: ServiceManager;
public deploymentManager: DeploymentManager;
public dnsManager: DnsManager;
public domainManager: DomainManager;
public taskManager: CloudlyTaskManager;
public nodeManager: CloudlyNodeManager; public nodeManager: CloudlyNodeManager;
public baremetalManager: CloudlyBaremetalManager; public baremetalManager: CloudlyBaremetalManager;
@@ -89,7 +97,11 @@ export class Cloudly {
this.externalApiManager = new ExternalApiManager(this); this.externalApiManager = new ExternalApiManager(this);
this.externalRegistryManager = new ExternalRegistryManager(this); this.externalRegistryManager = new ExternalRegistryManager(this);
this.imageManager = new ImageManager(this); this.imageManager = new ImageManager(this);
this.taskManager = new CloudlyTaskmanager(this); this.serviceManager = new ServiceManager(this);
this.deploymentManager = new DeploymentManager(this);
this.dnsManager = new DnsManager(this);
this.domainManager = new DomainManager(this);
this.taskManager = new CloudlyTaskManager(this);
this.secretManager = new CloudlySecretManager(this); this.secretManager = new CloudlySecretManager(this);
this.nodeManager = new CloudlyNodeManager(this); this.nodeManager = new CloudlyNodeManager(this);
this.baremetalManager = new CloudlyBaremetalManager(this); this.baremetalManager = new CloudlyBaremetalManager(this);
@@ -114,6 +126,9 @@ export class Cloudly {
await this.secretManager.start(); await this.secretManager.start();
await this.nodeManager.start(); await this.nodeManager.start();
await this.baremetalManager.start(); await this.baremetalManager.start();
await this.serviceManager.start();
await this.deploymentManager.start();
await this.taskManager.init();
await this.cloudflareConnector.init(); await this.cloudflareConnector.init();
await this.letsencryptConnector.init(); await this.letsencryptConnector.init();
@@ -123,6 +138,7 @@ export class Cloudly {
// start the managers // start the managers
this.imageManager.start(); this.imageManager.start();
this.externalRegistryManager.start();
} }
/** /**
@@ -133,5 +149,9 @@ export class Cloudly {
await this.letsencryptConnector.stop(); await this.letsencryptConnector.stop();
await this.mongodbConnector.stop(); await this.mongodbConnector.stop();
await this.secretManager.stop(); await this.secretManager.stop();
await this.serviceManager.stop();
await this.deploymentManager.stop();
await this.taskManager.stop();
await this.externalRegistryManager.stop();
} }
} }

View File

@@ -0,0 +1,98 @@
import * as plugins from '../plugins.js';
@plugins.smartdata.managed()
export class Deployment extends plugins.smartdata.SmartDataDbDoc<
Deployment,
plugins.servezoneInterfaces.data.IDeployment
> {
@plugins.smartdata.unI()
public id: string = plugins.smartunique.uniSimple('deployment');
@plugins.smartdata.svDb()
public serviceId: string;
@plugins.smartdata.svDb()
public nodeId: string;
@plugins.smartdata.svDb()
public containerId?: string;
@plugins.smartdata.svDb()
public usedImageId: string;
@plugins.smartdata.svDb()
public version: string;
@plugins.smartdata.svDb()
public deployedAt: number;
@plugins.smartdata.svDb()
public deploymentLog: string[] = [];
@plugins.smartdata.svDb()
public status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
@plugins.smartdata.svDb()
public healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
@plugins.smartdata.svDb()
public resourceUsage?: {
cpuUsagePercent: number;
memoryUsedMB: number;
lastUpdated: number;
};
public static async createDeployment(
deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>
): Promise<Deployment> {
const deployment = new Deployment();
if (deploymentData.serviceId) deployment.serviceId = deploymentData.serviceId;
if (deploymentData.nodeId) deployment.nodeId = deploymentData.nodeId;
if (deploymentData.containerId) deployment.containerId = deploymentData.containerId;
if (deploymentData.usedImageId) deployment.usedImageId = deploymentData.usedImageId;
if (deploymentData.version) deployment.version = deploymentData.version;
if (deploymentData.deployedAt) deployment.deployedAt = deploymentData.deployedAt;
if (deploymentData.deploymentLog) deployment.deploymentLog = deploymentData.deploymentLog;
if (deploymentData.status) deployment.status = deploymentData.status;
if (deploymentData.healthStatus) deployment.healthStatus = deploymentData.healthStatus;
if (deploymentData.resourceUsage) deployment.resourceUsage = deploymentData.resourceUsage;
await deployment.save();
return deployment;
}
public async updateHealthStatus(healthStatus: 'healthy' | 'unhealthy' | 'unknown') {
this.healthStatus = healthStatus;
await this.save();
}
public async updateResourceUsage(cpuUsagePercent: number, memoryUsedMB: number) {
this.resourceUsage = {
cpuUsagePercent,
memoryUsedMB,
lastUpdated: Date.now(),
};
await this.save();
}
public async addLogEntry(entry: string) {
this.deploymentLog.push(entry);
await this.save();
}
public async createSavableObject(): Promise<plugins.servezoneInterfaces.data.IDeployment> {
return {
id: this.id,
serviceId: this.serviceId,
nodeId: this.nodeId,
containerId: this.containerId,
usedImageId: this.usedImageId,
version: this.version,
deployedAt: this.deployedAt,
deploymentLog: this.deploymentLog,
status: this.status,
healthStatus: this.healthStatus,
resourceUsage: this.resourceUsage,
};
}
}

View File

@@ -0,0 +1,324 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import { Deployment } from './classes.deployment.js';
export class DeploymentManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CDeployment = plugins.smartdata.setDefaultManagerForDoc(this, Deployment);
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
// Connect typedrouter to main router
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
// Get all deployments
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(
'getDeployments',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployments = await this.CDeployment.getInstances({});
return {
deployments: await Promise.all(
deployments.map((deployment) => deployment.createSavableObject())
),
};
}
)
);
// Get deployment by ID
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(
'getDeploymentById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
return {
deployment: await deployment.createSavableObject(),
};
}
)
);
// Get deployments by service
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentsByService>(
'getDeploymentsByService',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployments = await this.CDeployment.getInstances({
serviceId: reqArg.serviceId,
});
return {
deployments: await Promise.all(
deployments.map((deployment) => deployment.createSavableObject())
),
};
}
)
);
// Get deployments by node
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentsByNode>(
'getDeploymentsByNode',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployments = await this.CDeployment.getInstances({
nodeId: reqArg.nodeId,
});
return {
deployments: await Promise.all(
deployments.map((deployment) => deployment.createSavableObject())
),
};
}
)
);
// Create deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(
'createDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await Deployment.createDeployment(reqArg.deploymentData);
return {
deployment: await deployment.createSavableObject(),
};
}
)
);
// Update deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(
'updateDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
// Update fields
if (reqArg.deploymentData.status !== undefined) {
deployment.status = reqArg.deploymentData.status;
}
if (reqArg.deploymentData.healthStatus !== undefined) {
deployment.healthStatus = reqArg.deploymentData.healthStatus;
}
if (reqArg.deploymentData.containerId !== undefined) {
deployment.containerId = reqArg.deploymentData.containerId;
}
if (reqArg.deploymentData.resourceUsage !== undefined) {
deployment.resourceUsage = reqArg.deploymentData.resourceUsage;
}
await deployment.save();
return {
deployment: await deployment.createSavableObject(),
};
}
)
);
// Delete deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(
'deleteDeploymentById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
const serviceId = deployment.serviceId;
await deployment.delete();
// Check if this was the last deployment for the service
const remainingDeployments = await this.getDeploymentsForService(serviceId);
if (remainingDeployments.length === 0) {
// Deactivate DNS entries if no more deployments exist
await this.cloudlyRef.dnsManager.deactivateServiceDnsEntries(serviceId);
}
return {
success: true,
};
}
)
);
// Restart deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(
'restartDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
// TODO: Implement actual restart logic with Docker/container runtime
deployment.status = 'starting';
await deployment.save();
return {
success: true,
deployment: await deployment.createSavableObject(),
};
}
)
);
// Scale deployment
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(
'scaleDeployment',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
// TODO: Implement scaling logic
// This would create/delete deployment instances based on replicas count
const deployment = await this.CDeployment.getInstance({
id: reqArg.deploymentId,
});
if (!deployment) {
throw new Error('Deployment not found');
}
return {
success: true,
deployment: await deployment.createSavableObject(),
};
}
)
);
}
/**
* Get all deployments
*/
public async getAllDeployments(): Promise<Deployment[]> {
return await this.CDeployment.getInstances({});
}
/**
* Get deployments for a specific service
*/
public async getDeploymentsForService(serviceId: string): Promise<Deployment[]> {
return await this.CDeployment.getInstances({
serviceId,
});
}
/**
* Get deployments for a specific node
*/
public async getDeploymentsForNode(nodeId: string): Promise<Deployment[]> {
return await this.CDeployment.getInstances({
nodeId,
});
}
/**
* Create a new deployment
*/
public async createDeployment(
serviceId: string,
nodeId: string,
version: string = 'latest'
): Promise<Deployment> {
const deployment = await Deployment.createDeployment({
serviceId,
nodeId,
version,
status: 'scheduled',
deployedAt: Date.now(),
deploymentLog: [`Deployment created at ${new Date().toISOString()}`],
});
// Activate DNS entries for the service
await this.cloudlyRef.dnsManager.activateServiceDnsEntries(serviceId);
// Get the node's IP address and update DNS entries
const node = await this.cloudlyRef.nodeManager.CClusterNode.getInstance({
id: nodeId,
});
if (node && node.data.publicIp) {
await this.cloudlyRef.dnsManager.updateServiceDnsEntriesIp(serviceId, node.data.publicIp);
}
return deployment;
}
public async start() {
// DeploymentManager is ready - handlers are already registered in constructor
console.log('DeploymentManager started');
}
public async stop() {
// Cleanup if needed
console.log('DeploymentManager stopped');
}
}

View File

@@ -0,0 +1,149 @@
import * as plugins from '../plugins.js';
import { DnsManager } from './classes.dnsmanager.js';
@plugins.smartdata.managed()
export class DnsEntry extends plugins.smartdata.SmartDataDbDoc<
DnsEntry,
plugins.servezoneInterfaces.data.IDnsEntry,
DnsManager
> {
// STATIC
public static async getDnsEntryById(dnsEntryIdArg: string) {
const dnsEntry = await this.getInstance({
id: dnsEntryIdArg,
});
return dnsEntry;
}
public static async getDnsEntries(filterArg?: { zone?: string }) {
const filter: any = {};
if (filterArg?.zone) {
filter['data.zone'] = filterArg.zone;
}
const dnsEntries = await this.getInstances(filter);
return dnsEntries;
}
public static async getDnsZones() {
const dnsEntries = await this.getInstances({});
const zones = new Set<string>();
for (const entry of dnsEntries) {
if (entry.data.zone) {
zones.add(entry.data.zone);
}
}
return Array.from(zones).sort();
}
public static async createDnsEntry(dnsEntryDataArg: plugins.servezoneInterfaces.data.IDnsEntry['data']) {
const dnsEntry = new DnsEntry();
dnsEntry.id = await DnsEntry.getNewId();
dnsEntry.data = {
...dnsEntryDataArg,
ttl: dnsEntryDataArg.ttl || 3600, // Default TTL: 1 hour
active: dnsEntryDataArg.active !== false, // Default to active
createdAt: Date.now(),
updatedAt: Date.now(),
};
await dnsEntry.save();
return dnsEntry;
}
public static async updateDnsEntry(
dnsEntryIdArg: string,
dnsEntryDataArg: Partial<plugins.servezoneInterfaces.data.IDnsEntry['data']>
) {
const dnsEntry = await this.getInstance({
id: dnsEntryIdArg,
});
if (!dnsEntry) {
throw new Error(`DNS entry with id ${dnsEntryIdArg} not found`);
}
Object.assign(dnsEntry.data, dnsEntryDataArg, {
updatedAt: Date.now(),
});
await dnsEntry.save();
return dnsEntry;
}
public static async deleteDnsEntry(dnsEntryIdArg: string) {
const dnsEntry = await this.getInstance({
id: dnsEntryIdArg,
});
if (!dnsEntry) {
throw new Error(`DNS entry with id ${dnsEntryIdArg} not found`);
}
await dnsEntry.delete();
return true;
}
// INSTANCE
@plugins.smartdata.svDb()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IDnsEntry['data'];
/**
* Validates the DNS entry data
*/
public validateData(): boolean {
const { type, name, value, zone } = this.data;
// Basic validation
if (!type || !name || !value || !zone) {
return false;
}
// Type-specific validation
switch (type) {
case 'A':
// Validate IPv4 address
return /^(\d{1,3}\.){3}\d{1,3}$/.test(value);
case 'AAAA':
// Validate IPv6 address (simplified)
return /^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$/.test(value);
case 'MX':
// MX records must have priority
return this.data.priority !== undefined && this.data.priority >= 0;
case 'SRV':
// SRV records must have priority, weight, and port
return (
this.data.priority !== undefined &&
this.data.weight !== undefined &&
this.data.port !== undefined
);
case 'CNAME':
case 'NS':
case 'PTR':
// These should point to valid domain names
return /^[a-zA-Z0-9.-]+$/.test(value);
case 'TXT':
case 'CAA':
case 'SOA':
// These can contain any text
return true;
default:
return false;
}
}
/**
* Get a formatted string representation of the DNS entry
*/
public toFormattedString(): string {
const { type, name, value, ttl, priority } = this.data;
let result = `${name} ${ttl} IN ${type}`;
if (priority !== undefined) {
result += ` ${priority}`;
}
if (type === 'SRV' && this.data.weight !== undefined && this.data.port !== undefined) {
result += ` ${this.data.weight} ${this.data.port}`;
}
result += ` ${value}`;
return result;
}
}

View File

@@ -0,0 +1,267 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import { DnsEntry } from './classes.dnsentry.js';
export class DnsManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CDnsEntry = plugins.smartdata.setDefaultManagerForDoc(this, DnsEntry);
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
// Get all DNS entries
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(
'getDnsEntries',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const dnsEntries = await this.CDnsEntry.getDnsEntries(
reqArg.zone ? { zone: reqArg.zone } : undefined
);
return {
dnsEntries: await Promise.all(
dnsEntries.map((entry) => entry.createSavableObject())
),
};
}
)
);
// Get DNS entry by ID
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(
'getDnsEntryById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const dnsEntry = await this.CDnsEntry.getDnsEntryById(reqArg.dnsEntryId);
if (!dnsEntry) {
throw new Error(`DNS entry with id ${reqArg.dnsEntryId} not found`);
}
return {
dnsEntry: await dnsEntry.createSavableObject(),
};
}
)
);
// Create DNS entry
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(
'createDnsEntry',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
// Validate domain exists and is activated if domainId is provided
if (reqArg.dnsEntryData.domainId) {
const domain = await this.cloudlyRef.domainManager.CDomain.getDomainById(reqArg.dnsEntryData.domainId);
if (!domain) {
throw new Error(`Domain with id ${reqArg.dnsEntryData.domainId} not found`);
}
if ((domain.data as any).activationState !== 'activated') {
throw new Error(`Domain ${domain.data.name} is not activated; DNS changes are not allowed.`);
}
// Set the zone from the domain name
reqArg.dnsEntryData.zone = domain.data.name;
}
const dnsEntry = await this.CDnsEntry.createDnsEntry(reqArg.dnsEntryData);
return {
dnsEntry: await dnsEntry.createSavableObject(),
};
}
)
);
// Update DNS entry
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(
'updateDnsEntry',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
// Validate domain exists and is activated if domainId is provided
if (reqArg.dnsEntryData.domainId) {
const domain = await this.cloudlyRef.domainManager.CDomain.getDomainById(reqArg.dnsEntryData.domainId);
if (!domain) {
throw new Error(`Domain with id ${reqArg.dnsEntryData.domainId} not found`);
}
if ((domain.data as any).activationState !== 'activated') {
throw new Error(`Domain ${domain.data.name} is not activated; DNS changes are not allowed.`);
}
// Set the zone from the domain name
reqArg.dnsEntryData.zone = domain.data.name;
}
const dnsEntry = await this.CDnsEntry.updateDnsEntry(
reqArg.dnsEntryId,
reqArg.dnsEntryData
);
return {
dnsEntry: await dnsEntry.createSavableObject(),
};
}
)
);
// Delete DNS entry
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(
'deleteDnsEntry',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const success = await this.CDnsEntry.deleteDnsEntry(reqArg.dnsEntryId);
return {
success,
};
}
)
);
// Get DNS zones
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(
'getDnsZones',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const zones = await this.CDnsEntry.getDnsZones();
return {
zones,
};
}
)
);
}
/**
* Create a DNS entry for a service
* @param dnsEntryData The DNS entry data
*/
public async createServiceDnsEntry(dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']) {
// If domainId is provided, get the domain and set the zone
if (dnsEntryData.domainId) {
const domain = await this.cloudlyRef.domainManager.CDomain.getInstance({
id: dnsEntryData.domainId,
});
if (domain) {
dnsEntryData.zone = domain.data.name;
}
}
// Create the DNS entry
const dnsEntry = await this.CDnsEntry.createDnsEntry(dnsEntryData);
return dnsEntry;
}
/**
* Activate DNS entries for a service when it's deployed
* @param serviceId The service ID
*/
public async activateServiceDnsEntries(serviceId: string) {
const dnsEntries = await this.CDnsEntry.getInstances({
'data.sourceServiceId': serviceId,
'data.sourceType': 'service',
});
for (const entry of dnsEntries) {
entry.data.active = true;
entry.data.updatedAt = Date.now();
await entry.save();
}
}
/**
* Deactivate DNS entries for a service when it's undeployed
* @param serviceId The service ID
*/
public async deactivateServiceDnsEntries(serviceId: string) {
const dnsEntries = await this.CDnsEntry.getInstances({
'data.sourceServiceId': serviceId,
'data.sourceType': 'service',
});
for (const entry of dnsEntries) {
entry.data.active = false;
entry.data.updatedAt = Date.now();
await entry.save();
}
}
/**
* Remove all DNS entries for a service
* @param serviceId The service ID
*/
public async removeServiceDnsEntries(serviceId: string) {
const dnsEntries = await this.CDnsEntry.getInstances({
'data.sourceServiceId': serviceId,
'data.sourceType': 'service',
});
for (const entry of dnsEntries) {
await entry.delete();
}
}
/**
* Update DNS entry values when deployment happens
* @param serviceId The service ID
* @param ipAddress The IP address to set for the DNS entries
*/
public async updateServiceDnsEntriesIp(serviceId: string, ipAddress: string) {
const dnsEntries = await this.CDnsEntry.getInstances({
'data.sourceServiceId': serviceId,
'data.sourceType': 'service',
});
for (const entry of dnsEntries) {
if (entry.data.type === 'A' || entry.data.type === 'AAAA') {
entry.data.value = ipAddress;
entry.data.updatedAt = Date.now();
await entry.save();
}
}
}
/**
* Initialize the DNS manager
*/
public async init() {
console.log('DNS Manager initialized');
}
/**
* Stop the DNS manager
*/
public async stop() {
console.log('DNS Manager stopped');
}
}

View File

@@ -0,0 +1,211 @@
import * as plugins from '../plugins.js';
import { DomainManager } from './classes.domainmanager.js';
@plugins.smartdata.managed()
export class Domain extends plugins.smartdata.SmartDataDbDoc<
Domain,
plugins.servezoneInterfaces.data.IDomain,
DomainManager
> {
// STATIC
public static async getDomainById(domainIdArg: string) {
const domain = await this.getInstance({
id: domainIdArg,
});
return domain;
}
public static async getDomainByName(domainNameArg: string) {
const domain = await this.getInstance({
'data.name': domainNameArg,
});
return domain;
}
public static async getDomains() {
const domains = await this.getInstances({});
return domains;
}
public static async createDomain(domainDataArg: plugins.servezoneInterfaces.data.IDomain['data']) {
const domain = new Domain();
domain.id = await Domain.getNewId();
domain.data = {
...domainDataArg,
status: domainDataArg.status || 'pending',
verificationStatus: domainDataArg.verificationStatus || 'pending',
nameservers: domainDataArg.nameservers || [],
autoRenew: domainDataArg.autoRenew !== false,
activationState: domainDataArg.activationState || 'available',
syncSource: domainDataArg.syncSource ?? null,
lastSyncAt: domainDataArg.lastSyncAt,
createdAt: Date.now(),
updatedAt: Date.now(),
};
await domain.save();
return domain;
}
public static async updateDomain(
domainIdArg: string,
domainDataArg: Partial<plugins.servezoneInterfaces.data.IDomain['data']>
) {
const domain = await this.getInstance({
id: domainIdArg,
});
if (!domain) {
throw new Error(`Domain with id ${domainIdArg} not found`);
}
// Merge updates and respect incoming activationState when provided
Object.assign(domain.data, domainDataArg);
domain.data.updatedAt = Date.now();
// Ensure activationState has a sensible default if still missing
if (!domain.data.activationState) {
(domain.data as any).activationState = 'available';
}
await domain.save();
return domain;
}
public static async deleteDomain(domainIdArg: string) {
const domain = await this.getInstance({
id: domainIdArg,
});
if (!domain) {
throw new Error(`Domain with id ${domainIdArg} not found`);
}
// Check if there are DNS entries for this domain
const dnsManager = domain.manager.cloudlyRef.dnsManager;
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
'data.zone': domain.data.name,
});
if (dnsEntries.length > 0) {
console.log(`Warning: Deleting domain ${domain.data.name} with ${dnsEntries.length} DNS entries`);
// Optionally delete associated DNS entries
for (const dnsEntry of dnsEntries) {
await dnsEntry.delete();
}
}
await domain.delete();
return true;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IDomain['data'];
/**
* Verify domain ownership
*/
public async verifyDomain(methodArg?: 'dns' | 'http' | 'email' | 'manual') {
const method = methodArg || this.data.verificationMethod || 'dns';
// Generate verification token if not exists
if (!this.data.verificationToken) {
this.data.verificationToken = plugins.smartunique.shortId();
await this.save();
}
let verificationResult = {
success: false,
message: '',
details: {} as any,
};
switch (method) {
case 'dns':
// Check for TXT record with verification token
verificationResult = await this.verifyViaDns();
break;
case 'http':
// Check for file at well-known URL
verificationResult = await this.verifyViaHttp();
break;
case 'email':
// Send verification email
verificationResult = await this.verifyViaEmail();
break;
case 'manual':
// Manual verification
verificationResult.success = true;
verificationResult.message = 'Manually verified';
break;
}
// Update verification status
if (verificationResult.success) {
this.data.verificationStatus = 'verified';
this.data.lastVerificationAt = Date.now();
this.data.verificationMethod = method;
} else {
this.data.verificationStatus = 'failed';
this.data.lastVerificationAt = Date.now();
}
await this.save();
return verificationResult;
}
private async verifyViaDns(): Promise<{ success: boolean; message: string; details: any }> {
// TODO: Implement DNS verification
// Look for TXT record _cloudly-verify.{domain} with value {verificationToken}
return {
success: false,
message: 'DNS verification not yet implemented',
details: {
expectedRecord: `_cloudly-verify.${this.data.name}`,
expectedValue: this.data.verificationToken,
},
};
}
private async verifyViaHttp(): Promise<{ success: boolean; message: string; details: any }> {
// TODO: Implement HTTP verification
// Check for file at http://{domain}/.well-known/cloudly-verify.txt
return {
success: false,
message: 'HTTP verification not yet implemented',
details: {
expectedUrl: `http://${this.data.name}/.well-known/cloudly-verify.txt`,
expectedContent: this.data.verificationToken,
},
};
}
private async verifyViaEmail(): Promise<{ success: boolean; message: string; details: any }> {
// TODO: Implement email verification
return {
success: false,
message: 'Email verification not yet implemented',
details: {},
};
}
/**
* Check if domain is expiring soon
*/
public isExpiringSoon(daysThreshold: number = 30): boolean {
if (!this.data.expiresAt) {
return false;
}
const daysUntilExpiry = (this.data.expiresAt - Date.now()) / (1000 * 60 * 60 * 24);
return daysUntilExpiry <= daysThreshold;
}
/**
* Get all DNS entries for this domain
*/
public async getDnsEntries() {
const dnsManager = this.manager.cloudlyRef.dnsManager;
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
'data.zone': this.data.name,
});
return dnsEntries;
}
}

View File

@@ -0,0 +1,188 @@
import type { Cloudly } from '../classes.cloudly.js';
import * as plugins from '../plugins.js';
import { Domain } from './classes.domain.js';
export class DomainManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CDomain = plugins.smartdata.setDefaultManagerForDoc(this, Domain);
constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
// Get all domains
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(
'getDomains',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const domains = await this.CDomain.getDomains();
return {
domains: await Promise.all(
domains.map((domain) => domain.createSavableObject())
),
};
}
)
);
// Get domain by ID
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(
'getDomainById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const domain = await this.CDomain.getDomainById(reqArg.domainId);
if (!domain) {
throw new Error(`Domain with id ${reqArg.domainId} not found`);
}
return {
domain: await domain.createSavableObject(),
};
}
)
);
// Create domain
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(
'createDomain',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
// Check if domain already exists
const existingDomain = await this.CDomain.getDomainByName(reqArg.domainData.name);
if (existingDomain) {
throw new Error(`Domain ${reqArg.domainData.name} already exists`);
}
const domain = await this.CDomain.createDomain(reqArg.domainData);
return {
domain: await domain.createSavableObject(),
};
}
)
);
// Update domain
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(
'updateDomain',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const domain = await this.CDomain.updateDomain(
reqArg.domainId,
reqArg.domainData
);
return {
domain: await domain.createSavableObject(),
};
}
)
);
// Delete domain
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(
'deleteDomain',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const success = await this.CDomain.deleteDomain(reqArg.domainId);
return {
success,
};
}
)
);
// Verify domain
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(
'verifyDomain',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const domain = await this.CDomain.getDomainById(reqArg.domainId);
if (!domain) {
throw new Error(`Domain with id ${reqArg.domainId} not found`);
}
const verificationResult = await domain.verifyDomain(reqArg.verificationMethod);
return {
domain: await domain.createSavableObject(),
verificationResult,
};
}
)
);
}
/**
* Initialize the domain manager
*/
public async init() {
console.log('Domain Manager initialized');
}
/**
* Stop the domain manager
*/
public async stop() {
console.log('Domain Manager stopped');
}
/**
* Get all active domains
*/
public async getActiveDomains() {
const domains = await this.CDomain.getInstances({
'data.status': 'active',
});
return domains;
}
/**
* Get domains that are expiring soon
*/
public async getExpiringDomains(daysThreshold: number = 30) {
const domains = await this.CDomain.getDomains();
return domains.filter(domain => domain.isExpiringSoon(daysThreshold));
}
/**
* Check if a domain name is available (not in our system)
*/
public async isDomainAvailable(domainName: string): Promise<boolean> {
const existingDomain = await this.CDomain.getDomainByName(domainName);
return !existingDomain;
}
}

View File

@@ -3,6 +3,7 @@ import * as paths from '../paths.js';
import type { Cloudly } from 'ts/classes.cloudly.js'; import type { Cloudly } from 'ts/classes.cloudly.js';
import type { ExternalRegistryManager } from './classes.externalregistrymanager.js'; import type { ExternalRegistryManager } from './classes.externalregistrymanager.js';
@plugins.smartdata.managed()
export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> { export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> {
// STATIC // STATIC
public static async getRegistryById(registryIdArg: string) { public static async getRegistryById(registryIdArg: string) {
@@ -17,14 +18,92 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
return externalRegistries; return externalRegistries;
} }
public static async getDefaultRegistry(type: 'docker' | 'npm' = 'docker') {
const defaultRegistry = await this.getInstance({
'data.type': type,
'data.isDefault': true,
});
return defaultRegistry;
}
public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) { public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) {
const externalRegistry = new ExternalRegistry(); const externalRegistry = new ExternalRegistry();
externalRegistry.id = await ExternalRegistry.getNewId(); externalRegistry.id = await this.getNewId();
Object.assign(externalRegistry, registryDataArg); externalRegistry.data = {
type: registryDataArg.type || 'docker',
name: registryDataArg.name || '',
url: registryDataArg.url || '',
username: registryDataArg.username,
password: registryDataArg.password,
description: registryDataArg.description,
isDefault: registryDataArg.isDefault || false,
authType: registryDataArg.authType || (registryDataArg.username || registryDataArg.password ? 'basic' : 'none'),
insecure: registryDataArg.insecure || false,
namespace: registryDataArg.namespace,
proxy: registryDataArg.proxy,
config: registryDataArg.config,
status: 'unverified',
createdAt: Date.now(),
updatedAt: Date.now(),
};
// If this is set as default, unset other defaults of the same type
if (externalRegistry.data.isDefault) {
const existingDefaults = await this.getInstances({
'data.type': externalRegistry.data.type,
'data.isDefault': true,
});
for (const existingDefault of existingDefaults) {
existingDefault.data.isDefault = false;
await existingDefault.save();
}
}
await externalRegistry.save(); await externalRegistry.save();
return externalRegistry; return externalRegistry;
} }
public static async updateExternalRegistry(
registryIdArg: string,
registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>
) {
const externalRegistry = await this.getRegistryById(registryIdArg);
if (!externalRegistry) {
throw new Error(`Registry with id ${registryIdArg} not found`);
}
// If setting as default, unset other defaults of the same type
if (registryDataArg.isDefault && !externalRegistry.data.isDefault) {
const existingDefaults = await this.getInstances({
'data.type': externalRegistry.data.type,
'data.isDefault': true,
});
for (const existingDefault of existingDefaults) {
if (existingDefault.id !== registryIdArg) {
existingDefault.data.isDefault = false;
await existingDefault.save();
}
}
}
// Update fields
Object.assign(externalRegistry.data, registryDataArg, {
updatedAt: Date.now(),
});
await externalRegistry.save();
return externalRegistry;
}
public static async deleteExternalRegistry(registryIdArg: string) {
const externalRegistry = await this.getRegistryById(registryIdArg);
if (!externalRegistry) {
return false;
}
await externalRegistry.delete();
return true;
}
// INSTANCE // INSTANCE
@plugins.smartdata.svDb() @plugins.smartdata.svDb()
@@ -37,4 +116,91 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR
super(); super();
} }
/**
* Verify the registry connection
*/
public async verifyConnection(): Promise<{ success: boolean; message?: string }> {
try {
// For Docker registries, try to access the v2 API
if (this.data.type === 'docker') {
const registryUrl = this.data.url.replace(/\/$/, ''); // Remove trailing slash
// Build headers based on auth type
const headers: any = {};
if (this.data.authType === 'basic' && this.data.username && this.data.password) {
headers['Authorization'] = 'Basic ' + Buffer.from(`${this.data.username}:${this.data.password}`).toString('base64');
} else if (this.data.authType === 'token' && this.data.password) {
// For token auth, password field contains the token
headers['Authorization'] = `Bearer ${this.data.password}`;
}
// For 'none' auth type or missing credentials, no auth header is added
// Try to access the Docker Registry v2 API
const response = await fetch(`${registryUrl}/v2/`, {
headers,
// Allow insecure if configured
...(this.data.insecure ? { rejectUnauthorized: false } : {}),
}).catch(err => {
throw new Error(`Failed to connect: ${err.message}`);
});
if (response.status === 200) {
// 200 means successful (either public or authenticated)
this.data.status = 'active';
this.data.lastVerified = Date.now();
this.data.lastError = undefined;
await this.save();
return { success: true, message: 'Registry connection successful' };
} else if (response.status === 401 && this.data.authType === 'none') {
// 401 with no auth means registry exists but needs auth
throw new Error('Registry requires authentication');
} else if (response.status === 401) {
throw new Error('Authentication failed - check credentials');
} else {
throw new Error(`Registry returned status ${response.status}`);
}
}
// For npm registries, implement npm-specific verification
if (this.data.type === 'npm') {
// TODO: Implement npm registry verification
this.data.status = 'unverified';
return { success: false, message: 'NPM registry verification not yet implemented' };
}
return { success: false, message: 'Unknown registry type' };
} catch (error) {
this.data.status = 'error';
this.data.lastError = error.message;
await this.save();
return { success: false, message: error.message };
}
}
/**
* Get the full registry URL with namespace if applicable
*/
public getFullRegistryUrl(): string {
let url = this.data.url.replace(/\/$/, ''); // Remove trailing slash
if (this.data.namespace) {
url = `${url}/${this.data.namespace}`;
}
return url;
}
/**
* Get Docker auth config for this registry
*/
public getDockerAuthConfig() {
if (this.data.type !== 'docker') {
return null;
}
return {
username: this.data.username,
password: this.data.password,
email: this.data.config?.dockerConfig?.email,
serveraddress: this.data.config?.dockerConfig?.serverAddress || this.data.url,
};
}
} }

View File

@@ -13,24 +13,35 @@ export class ExternalRegistryManager {
constructor(cloudlyRef: Cloudly) { constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef; this.cloudlyRef = cloudlyRef;
}
public async start() { // Add typedrouter to cloudly's main router
// lets set up a typedrouter this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedRouter(this.typedrouter);
// Get registry by ID
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>( this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistryById>(
new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => { new plugins.typedrequest.TypedHandler('getExternalRegistryById', async (dataArg) => {
const registry = await ExternalRegistry.getRegistryById(dataArg.id); await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const registry = await this.CExternalRegistry.getRegistryById(dataArg.id);
if (!registry) {
throw new Error(`Registry with id ${dataArg.id} not found`);
}
return { return {
registry: await registry.createSavableObject(), registry: await registry.createSavableObject(),
}; };
}) })
); );
// Get all registries
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>( this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_GetRegistries>(
new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => { new plugins.typedrequest.TypedHandler('getExternalRegistries', async (dataArg) => {
const registries = await ExternalRegistry.getRegistries(); await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const registries = await this.CExternalRegistry.getRegistries();
return { return {
registries: await Promise.all( registries: await Promise.all(
registries.map((registry) => registry.createSavableObject()) registries.map((registry) => registry.createSavableObject())
@@ -39,13 +50,81 @@ export class ExternalRegistryManager {
}) })
); );
// Create registry
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>( this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_CreateRegistry>(
new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => { new plugins.typedrequest.TypedHandler('createExternalRegistry', async (dataArg) => {
const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const registry = await this.CExternalRegistry.createExternalRegistry(dataArg.registryData);
return { return {
registry: await registry.createSavableObject(), registry: await registry.createSavableObject(),
}; };
}) })
); );
// Update registry
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(
new plugins.typedrequest.TypedHandler('updateExternalRegistry', async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const registry = await this.CExternalRegistry.updateExternalRegistry(
dataArg.registryId,
dataArg.registryData
);
return {
resultRegistry: await registry.createSavableObject(),
};
})
);
// Delete registry
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(
new plugins.typedrequest.TypedHandler('deleteExternalRegistryById', async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const success = await this.CExternalRegistry.deleteExternalRegistry(dataArg.registryId);
return {
ok: success,
};
})
);
// Verify registry connection
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>(
new plugins.typedrequest.TypedHandler('verifyExternalRegistry', async (dataArg) => {
await plugins.smartguard.passGuardsOrReject(dataArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const registry = await this.CExternalRegistry.getRegistryById(dataArg.registryId);
if (!registry) {
return {
success: false,
message: `Registry with id ${dataArg.registryId} not found`,
};
}
const result = await registry.verifyConnection();
return {
success: result.success,
message: result.message,
registry: await registry.createSavableObject(),
};
})
);
}
public async start() {
console.log('External Registry Manager started');
}
public async stop() {
console.log('External Registry Manager stopped');
} }
} }

View File

@@ -2,6 +2,7 @@ import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js';
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import { ServiceManager } from './classes.servicemanager.js'; import { ServiceManager } from './classes.servicemanager.js';
@plugins.smartdata.managed()
export class Service extends plugins.smartdata.SmartDataDbDoc< export class Service extends plugins.smartdata.SmartDataDbDoc<
Service, Service,
plugins.servezoneInterfaces.data.IService, plugins.servezoneInterfaces.data.IService,
@@ -25,6 +26,12 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
service.id = await Service.getNewId(); service.id = await Service.getNewId();
Object.assign(service, serviceDataArg); Object.assign(service, serviceDataArg);
await service.save(); await service.save();
// Create DNS entries if service has web port and domains configured
if (service.data.ports?.web && service.data.domains?.length > 0) {
await service.createDnsEntries();
}
return service; return service;
} }
@@ -54,4 +61,40 @@ export class Service extends plugins.smartdata.SmartDataDbDoc<
} }
return finalFlatObject; return finalFlatObject;
} }
/**
* Creates DNS entries for this service (in inactive state)
* These will be activated when the service is deployed
*/
public async createDnsEntries() {
const dnsManager = this.manager.cloudlyRef.dnsManager;
for (const domain of this.data.domains) {
const dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data'] = {
type: 'A', // Default to A record, could be made configurable
name: domain.name,
value: '0.0.0.0', // Placeholder, will be updated on deployment
ttl: 3600,
zone: '', // Will be set based on domainId
domainId: domain.domainId,
active: false, // Created as inactive
description: `Auto-generated DNS entry for service ${this.data.name}`,
createdAt: Date.now(),
isAutoGenerated: true,
sourceServiceId: this.id,
sourceType: 'service',
};
// Create the DNS entry
await dnsManager.createServiceDnsEntry(dnsEntryData);
}
}
/**
* Removes DNS entries for this service
*/
public async removeDnsEntries() {
const dnsManager = this.manager.cloudlyRef.dnsManager;
await dnsManager.removeServiceDnsEntries(this.id);
}
} }

View File

@@ -15,6 +15,8 @@ export class ServiceManager {
constructor(cloudlyRef: Cloudly) { constructor(cloudlyRef: Cloudly) {
this.cloudlyRef = cloudlyRef; this.cloudlyRef = cloudlyRef;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
this.typedrouter.addTypedHandler( this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>( new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_GetServices>(
'getServices', 'getServices',
@@ -89,6 +91,10 @@ export class ServiceManager {
const service = await Service.getInstance({ const service = await Service.getInstance({
id: dataArg.serviceId, id: dataArg.serviceId,
}); });
// Remove DNS entries before deleting the service
await service.removeDnsEntries();
await service.delete(); await service.delete();
return { return {
success: true, success: true,
@@ -97,4 +103,14 @@ export class ServiceManager {
) )
); );
} }
public async start() {
// ServiceManager is ready - handlers are already registered in constructor
console.log('ServiceManager started');
}
public async stop() {
// Cleanup if needed
console.log('ServiceManager stopped');
}
} }

View File

@@ -0,0 +1,165 @@
import * as plugins from '../plugins.js';
import { CloudlyTaskManager } from './classes.taskmanager.js';
@plugins.smartdata.managed()
export class TaskExecution extends plugins.smartdata.SmartDataDbDoc<
TaskExecution,
plugins.servezoneInterfaces.data.ITaskExecution,
CloudlyTaskManager
> {
// STATIC
public static async getTaskExecutionById(executionIdArg: string) {
const execution = await this.getInstance({
id: executionIdArg,
});
return execution;
}
public static async getTaskExecutions(filterArg?: {
taskName?: string;
status?: string;
startedAfter?: number;
startedBefore?: number;
}) {
const query: any = {};
if (filterArg?.taskName) {
query['data.taskName'] = filterArg.taskName;
}
if (filterArg?.status) {
query['data.status'] = filterArg.status;
}
if (filterArg?.startedAfter || filterArg?.startedBefore) {
query['data.startedAt'] = {};
if (filterArg.startedAfter) {
query['data.startedAt'].$gte = filterArg.startedAfter;
}
if (filterArg.startedBefore) {
query['data.startedAt'].$lte = filterArg.startedBefore;
}
}
const executions = await this.getInstances(query);
return executions;
}
public static async createTaskExecution(
taskName: string,
triggeredBy: 'schedule' | 'manual' | 'system',
userId?: string
) {
const execution = new TaskExecution();
execution.id = await TaskExecution.getNewId();
execution.data = {
taskName,
startedAt: Date.now(),
status: 'running',
triggeredBy,
userId,
logs: [],
};
await execution.save();
return execution;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.ITaskExecution['data'];
/**
* Add a log entry to the execution
*/
public async addLog(message: string, severity: 'info' | 'warning' | 'error' | 'success' = 'info') {
this.data.logs.push({
timestamp: Date.now(),
message,
severity,
});
await this.save();
}
/**
* Set a metric for the execution
*/
public async setMetric(key: string, value: any) {
if (!this.data.metrics) {
this.data.metrics = {};
}
this.data.metrics[key] = value;
await this.save();
}
/**
* Mark the execution as completed
*/
public async complete(result?: any) {
this.data.completedAt = Date.now();
this.data.duration = this.data.completedAt - this.data.startedAt;
this.data.status = 'completed';
if (result !== undefined) {
this.data.result = result;
}
await this.save();
}
/**
* Mark the execution as failed
*/
public async fail(error: Error | string) {
this.data.completedAt = Date.now();
this.data.duration = this.data.completedAt - this.data.startedAt;
this.data.status = 'failed';
if (typeof error === 'string') {
this.data.error = {
message: error,
};
} else {
this.data.error = {
message: error.message,
stack: error.stack,
code: (error as any).code,
};
}
await this.save();
}
/**
* Cancel the execution
*/
public async cancel() {
this.data.completedAt = Date.now();
this.data.duration = this.data.completedAt - this.data.startedAt;
this.data.status = 'cancelled';
await this.save();
}
/**
* Get running executions
*/
public static async getRunningExecutions() {
return await this.getInstances({
'data.status': 'running',
});
}
/**
* Clean up old executions
*/
public static async cleanupOldExecutions(olderThanDays: number = 30) {
const cutoffTime = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000);
const oldExecutions = await this.getInstances({
'data.completedAt': { $lt: cutoffTime },
});
for (const execution of oldExecutions) {
await execution.delete();
}
return oldExecutions.length;
}
}

View File

@@ -0,0 +1,349 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { TaskExecution } from './classes.taskexecution.js';
import { createPredefinedTasks } from './predefinedtasks.js';
import { logger } from '../logger.js';
export interface ITaskInfo {
name: string;
description: string;
category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
schedule?: string; // Cron expression if scheduled
lastRun?: number;
enabled: boolean;
}
export class CloudlyTaskManager {
public typedrouter = new plugins.typedrequest.TypedRouter();
public cloudlyRef: Cloudly;
// TaskBuffer integration
private taskBufferManager = new plugins.taskbuffer.TaskManager();
private taskRegistry = new Map<string, plugins.taskbuffer.Task>();
private taskInfo = new Map<string, ITaskInfo>();
private currentExecutions = new Map<string, TaskExecution>();
private cancellationRequests = new Set<string>();
// Database connection helper
get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
// Set up TaskExecution document manager
public CTaskExecution = plugins.smartdata.setDefaultManagerForDoc(this, TaskExecution);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
// Add router to main router
this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
// Set up API endpoints
this.setupApiEndpoints();
// Register predefined tasks
createPredefinedTasks(this);
}
/**
* Register a task with the manager
*/
public registerTask(
name: string,
task: plugins.taskbuffer.Task,
info: Omit<ITaskInfo, 'name' | 'lastRun'>
) {
this.taskRegistry.set(name, task);
this.taskInfo.set(name, {
name,
...info,
lastRun: undefined,
});
// Schedule if cron expression provided
if (info.schedule && info.enabled) {
this.scheduleTask(name, info.schedule);
}
logger.log('info', `Registered task: ${name}`);
}
/**
* Execute a task with tracking
*/
public async executeTask(
taskName: string,
triggeredBy: 'schedule' | 'manual' | 'system',
userId?: string
): Promise<TaskExecution> {
const task = this.taskRegistry.get(taskName);
const info = this.taskInfo.get(taskName);
if (!task) {
throw new Error(`Task ${taskName} not found`);
}
if (!info?.enabled && triggeredBy === 'schedule') {
logger.log('warn', `Skipping disabled scheduled task: ${taskName}`);
return null;
}
// Create execution record
const execution = await TaskExecution.createTaskExecution(taskName, triggeredBy, userId);
if (info?.description) {
execution.data.taskDescription = info.description;
}
if (info?.category) {
execution.data.category = info.category;
}
await execution.save();
// Store current execution for task to access
this.currentExecutions.set(taskName, execution);
try {
await execution.addLog(`Starting task: ${taskName}`, 'info');
// Execute the task
const result = await task.trigger();
// If a cancellation was requested during execution, don't mark as completed
if (execution.data.status === 'cancelled' || this.cancellationRequests.has(execution.id)) {
await execution.addLog('Task cancelled during execution', 'warning');
} else {
// Task completed successfully
await execution.complete(result);
await execution.addLog(`Task completed successfully`, 'success');
}
// Update last run time
if (info) {
info.lastRun = Date.now();
}
} catch (error) {
// If already cancelled, don't mark as failed
if (execution.data.status === 'cancelled' || this.cancellationRequests.has(execution.id)) {
await execution.addLog('Task was cancelled', 'warning');
} else {
// Task failed
await execution.fail(error as any);
await execution.addLog(`Task failed: ${(error as any).message}`, 'error');
logger.log('error', `Task ${taskName} failed: ${(error as any).message}`);
}
} finally {
// Clean up current execution
this.currentExecutions.delete(taskName);
this.cancellationRequests.delete(execution.id);
}
return execution;
}
/**
* Get current execution for a task (used by tasks to log)
*/
public getCurrentExecution(taskName: string): TaskExecution | undefined {
return this.currentExecutions.get(taskName);
}
/**
* Schedule a task with cron expression
*/
public scheduleTask(taskName: string, cronExpression: string) {
const task = this.taskRegistry.get(taskName);
if (!task) {
throw new Error(`Task ${taskName} not found`);
}
// Wrap task execution with tracking
const wrappedTask = new plugins.taskbuffer.Task({
name: `${taskName}-scheduled`,
taskFunction: async () => {
await this.executeTask(taskName, 'schedule');
},
});
this.taskBufferManager.addAndScheduleTask(wrappedTask, cronExpression);
logger.log('info', `Scheduled task ${taskName} with cron: ${cronExpression}`);
}
/**
* Cancel a running task
*/
public async cancelTask(executionId: string): Promise<boolean> {
const execution = await TaskExecution.getTaskExecutionById(executionId);
if (!execution || execution.data.status !== 'running') {
return false;
}
await execution.cancel();
await execution.addLog('Task cancelled by user', 'warning');
// mark cancellation request so running task functions can react cooperatively
this.cancellationRequests.add(execution.id);
return true;
}
/**
* Check if cancellation is requested for an execution
*/
public isCancellationRequested(executionId: string): boolean {
return this.cancellationRequests.has(executionId);
}
/**
* Get all registered tasks
*/
public getAllTasks(): ITaskInfo[] {
return Array.from(this.taskInfo.values());
}
/**
* Enable or disable a task
*/
public async setTaskEnabled(taskName: string, enabled: boolean) {
const info = this.taskInfo.get(taskName);
if (!info) {
throw new Error(`Task ${taskName} not found`);
}
info.enabled = enabled;
if (!enabled) {
// TODO: Remove from scheduler if disabled
logger.log('info', `Disabled task: ${taskName}`);
} else if (info.schedule) {
// Reschedule if enabled with schedule
this.scheduleTask(taskName, info.schedule);
}
}
/**
* Set up API endpoints
*/
private setupApiEndpoints() {
// Get all tasks
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>(
'getTasks',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const tasks = this.getAllTasks();
return {
tasks,
};
}
)
);
// Get task executions
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>(
'getTaskExecutions',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const executions = await TaskExecution.getTaskExecutions(reqArg.filter);
return {
executions: await Promise.all(
executions.map(e => e.createSavableObject())
),
};
}
)
);
// Get task execution by ID
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>(
'getTaskExecutionById',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const execution = await TaskExecution.getTaskExecutionById(reqArg.executionId);
if (!execution) {
throw new Error('Task execution not found');
}
return {
execution: await execution.createSavableObject(),
};
}
)
);
// Trigger task manually
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>(
'triggerTask',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const execution = await this.executeTask(
reqArg.taskName,
'manual',
reqArg.userId
);
return {
execution: await execution.createSavableObject(),
};
}
)
);
// Cancel task
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>(
'cancelTask',
async (reqArg) => {
await plugins.smartguard.passGuardsOrReject(reqArg, [
this.cloudlyRef.authManager.validIdentityGuard,
]);
const success = await this.cancelTask(reqArg.executionId);
return {
success,
};
}
)
);
}
/**
* Initialize the task manager
*/
public async init() {
logger.log('info', 'Task Manager initialized');
// Clean up old executions on startup
const deletedCount = await TaskExecution.cleanupOldExecutions(30);
if (deletedCount > 0) {
logger.log('info', `Cleaned up ${deletedCount} old task executions`);
}
}
/**
* Stop the task manager
*/
public async stop() {
// Stop all scheduled tasks
await this.taskBufferManager.stop();
logger.log('info', 'Task Manager stopped');
}
}

View File

@@ -0,0 +1,566 @@
import * as plugins from '../plugins.js';
import { CloudlyTaskManager } from './classes.taskmanager.js';
import { logger } from '../logger.js';
/**
* Create and register all predefined tasks
*/
export function createPredefinedTasks(taskManager: CloudlyTaskManager) {
// Cloudflare Domain Sync Task
const cfDomainSync = new plugins.taskbuffer.Task({
name: 'cloudflare-domain-sync',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('cloudflare-domain-sync');
try {
await execution?.addLog('Starting Cloudflare domain sync…', 'info');
const cf = taskManager.cloudlyRef.cloudflareConnector?.cloudflare;
if (!cf) {
await execution?.addLog('Cloudflare not configured; skipping sync.', 'warning');
return { created: 0, updated: 0, totalZones: 0 };
}
const zones = await cf.convenience.listZones();
await execution?.setMetric('totalZones', zones.length);
await execution?.addLog(`Fetched ${zones.length} zones from Cloudflare`, 'info');
let created = 0;
let updated = 0;
const now = Date.now();
for (const zone of zones) {
// zone fields from Cloudflare typings
const zoneName = (zone as any).name as string;
const zoneId = (zone as any).id as string;
const zoneStatus = ((zone as any).status || 'active') as 'active'|'pending'|'suspended'|'transferred'|'expired';
const nameServers: string[] = (zone as any).name_servers || [];
const existing = await taskManager.cloudlyRef.domainManager.CDomain.getDomainByName(zoneName);
if (existing) {
if (execution && (taskManager.isCancellationRequested(execution.id) || existing.data == null)) {
await execution?.addLog('Cancellation requested. Stopping CF sync…', 'warning');
break;
}
await execution?.addLog(`Updating domain: ${zoneName}`, 'info');
await taskManager.cloudlyRef.domainManager.CDomain.updateDomain(existing.id, {
status: zoneStatus as any,
nameservers: nameServers,
cloudflareZoneId: zoneId,
syncSource: 'cloudflare',
lastSyncAt: now,
activationState: existing.data.activationState || 'available',
});
updated++;
} else {
await execution?.addLog(`Creating domain: ${zoneName}`, 'info');
await taskManager.cloudlyRef.domainManager.CDomain.createDomain({
name: zoneName,
description: `Synced from Cloudflare zone ${zoneId}`,
status: zoneStatus as any,
verificationStatus: 'pending',
nameservers: nameServers,
autoRenew: true,
cloudflareZoneId: zoneId,
activationState: 'available',
syncSource: 'cloudflare',
lastSyncAt: now,
} as any);
created++;
}
}
await execution?.setMetric('created', created);
await execution?.setMetric('updated', updated);
await execution?.addLog(`Cloudflare sync done: ${created} created, ${updated} updated`, 'success');
return { created, updated, totalZones: zones.length };
} catch (error) {
await execution?.addLog(`Cloudflare sync error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('cloudflare-domain-sync', cfDomainSync, {
description: 'Import and update domains from Cloudflare zones',
category: 'system',
schedule: '0 3 * * *', // Daily at 3 AM
enabled: true,
});
// DNS Sync Task
const dnsSync = new plugins.taskbuffer.Task({
name: 'dns-sync',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('dns-sync');
const dnsManager = taskManager.cloudlyRef.dnsManager;
try {
await execution?.addLog('Starting DNS synchronization...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting DNS sync...', 'warning');
return;
}
// Get all DNS entries marked as external
const dnsEntries = await dnsManager.CDnsEntry.getInstances({
'data.sourceType': 'external',
});
await execution?.addLog(`Found ${dnsEntries.length} external DNS entries to sync`, 'info');
await execution?.setMetric('totalEntries', dnsEntries.length);
let syncedCount = 0;
let failedCount = 0;
for (const entry of dnsEntries) {
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
break;
}
try {
// TODO: Implement actual sync with external DNS provider
await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info');
syncedCount++;
} catch (error) {
await execution?.addLog(`Failed to sync ${entry.data.name}: ${error.message}`, 'warning');
failedCount++;
}
}
await execution?.setMetric('syncedCount', syncedCount);
await execution?.setMetric('failedCount', failedCount);
await execution?.addLog(`DNS sync completed: ${syncedCount} synced, ${failedCount} failed`, 'success');
return { synced: syncedCount, failed: failedCount };
} catch (error) {
await execution?.addLog(`DNS sync error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('dns-sync', dnsSync, {
description: 'Synchronize DNS entries with external providers',
category: 'system',
schedule: '0 */6 * * *', // Every 6 hours
enabled: true,
});
// Certificate Renewal Task
const certRenewal = new plugins.taskbuffer.Task({
name: 'cert-renewal',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('cert-renewal');
try {
await execution?.addLog('Checking certificates for renewal...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting certificate renewal...', 'warning');
return;
}
// Get all domains (only activated ones are considered for renewal)
const domains = await taskManager.cloudlyRef.domainManager.CDomain.getInstances({
'data.activationState': 'activated',
} as any);
await execution?.setMetric('totalDomains', domains.length);
let renewedCount = 0;
let upToDateCount = 0;
for (const domain of domains) {
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
break;
}
// TODO: Check certificate expiry and renew if needed
await execution?.addLog(`Checking certificate for ${domain.data.name}`, 'info');
// Placeholder logic
const needsRenewal = Math.random() > 0.8; // 20% chance for demo
if (needsRenewal) {
await execution?.addLog(`Renewing certificate for ${domain.data.name}`, 'info');
// TODO: Actual renewal logic
renewedCount++;
} else {
upToDateCount++;
}
}
await execution?.setMetric('renewedCount', renewedCount);
await execution?.setMetric('upToDateCount', upToDateCount);
await execution?.addLog(`Certificate check completed: ${renewedCount} renewed, ${upToDateCount} up to date`, 'success');
return { renewed: renewedCount, upToDate: upToDateCount };
} catch (error) {
await execution?.addLog(`Certificate renewal error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('cert-renewal', certRenewal, {
description: 'Check and renew SSL certificates',
category: 'security',
schedule: '0 2 * * *', // Daily at 2 AM
enabled: true,
});
// Cleanup Task
const cleanup = new plugins.taskbuffer.Task({
name: 'cleanup',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('cleanup');
try {
await execution?.addLog('Starting cleanup tasks...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting cleanup...', 'warning');
return;
}
// Clean up old task executions
await execution?.addLog('Cleaning old task executions...', 'info');
const deletedExecutions = await taskManager.CTaskExecution.cleanupOldExecutions(30);
await execution?.setMetric('deletedExecutions', deletedExecutions);
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
// TODO: Clean up old logs
await execution?.addLog('Cleaning old logs...', 'info');
// Placeholder
const deletedLogs = 0;
await execution?.setMetric('deletedLogs', deletedLogs);
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
// TODO: Clean up Docker images
await execution?.addLog('Cleaning unused Docker images...', 'info');
// Placeholder
const deletedImages = 0;
await execution?.setMetric('deletedImages', deletedImages);
await execution?.addLog(`Cleanup completed: ${deletedExecutions} executions, ${deletedLogs} logs, ${deletedImages} images deleted`, 'success');
return {
executions: deletedExecutions,
logs: deletedLogs,
images: deletedImages,
};
} catch (error) {
await execution?.addLog(`Cleanup error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('cleanup', cleanup, {
description: 'Remove old logs, executions, and temporary files',
category: 'cleanup',
schedule: '0 3 * * *', // Daily at 3 AM
enabled: true,
});
// Health Check Task
const healthCheck = new plugins.taskbuffer.Task({
name: 'health-check',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('health-check');
try {
await execution?.addLog('Starting health checks...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting health checks...', 'warning');
return;
}
// Check all deployments
const deployments = await taskManager.cloudlyRef.deploymentManager.getAllDeployments();
await execution?.setMetric('totalDeployments', deployments.length);
let healthyCount = 0;
let unhealthyCount = 0;
const issues = [];
for (const deployment of deployments) {
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
break;
}
if (deployment.status === 'running') {
// TODO: Actual health check logic
const isHealthy = Math.random() > 0.1; // 90% healthy for demo
if (isHealthy) {
healthyCount++;
} else {
unhealthyCount++;
issues.push({
deploymentId: deployment.id,
serviceId: deployment.serviceId,
issue: 'Health check failed',
});
await execution?.addLog(`Deployment ${deployment.id} is unhealthy`, 'warning');
}
}
}
await execution?.setMetric('healthyCount', healthyCount);
await execution?.setMetric('unhealthyCount', unhealthyCount);
await execution?.setMetric('issues', issues);
const severity = unhealthyCount > 0 ? 'warning' : 'success';
await execution?.addLog(
`Health check completed: ${healthyCount} healthy, ${unhealthyCount} unhealthy`,
severity as any
);
return { healthy: healthyCount, unhealthy: unhealthyCount, issues };
} catch (error) {
await execution?.addLog(`Health check error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('health-check', healthCheck, {
description: 'Monitor service health across deployments',
category: 'monitoring',
schedule: '*/15 * * * *', // Every 15 minutes
enabled: true,
});
// Resource Usage Report
const resourceReport = new plugins.taskbuffer.Task({
name: 'resource-report',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('resource-report');
try {
await execution?.addLog('Generating resource usage report...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting report...', 'warning');
return;
}
// Get all nodes
const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({});
const report = {
timestamp: Date.now(),
nodes: [],
totalCpu: 0,
totalMemory: 0,
totalDisk: 0,
};
for (const node of nodes) {
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
break;
}
// TODO: Get actual resource usage
const nodeUsage = {
nodeId: node.id,
nodeName: node.data.name,
cpu: Math.random() * 100, // Placeholder
memory: Math.random() * 100, // Placeholder
disk: Math.random() * 100, // Placeholder
};
report.nodes.push(nodeUsage);
report.totalCpu += nodeUsage.cpu;
report.totalMemory += nodeUsage.memory;
report.totalDisk += nodeUsage.disk;
}
// Calculate averages
if (nodes.length > 0) {
report.totalCpu /= nodes.length;
report.totalMemory /= nodes.length;
report.totalDisk /= nodes.length;
}
await execution?.setMetric('report', report);
await execution?.addLog(
`Resource report generated: Avg CPU ${report.totalCpu.toFixed(1)}%, Memory ${report.totalMemory.toFixed(1)}%, Disk ${report.totalDisk.toFixed(1)}%`,
'success'
);
return report;
} catch (error) {
await execution?.addLog(`Resource report error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('resource-report', resourceReport, {
description: 'Generate resource usage reports',
category: 'monitoring',
schedule: '0 * * * *', // Every hour
enabled: true,
});
// Database Maintenance
const dbMaintenance = new plugins.taskbuffer.Task({
name: 'db-maintenance',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('db-maintenance');
try {
await execution?.addLog('Starting database maintenance...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting DB maintenance...', 'warning');
return;
}
// TODO: Implement actual database maintenance
await execution?.addLog('Analyzing indexes...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
await execution?.addLog('Compacting collections...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) return;
await execution?.addLog('Updating statistics...', 'info');
await execution?.setMetric('collectionsOptimized', 5); // Placeholder
await execution?.setMetric('indexesRebuilt', 3); // Placeholder
await execution?.addLog('Database maintenance completed', 'success');
return { success: true };
} catch (error) {
await execution?.addLog(`Database maintenance error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('db-maintenance', dbMaintenance, {
description: 'Optimize database performance',
category: 'maintenance',
schedule: '0 4 * * 0', // Weekly on Sunday at 4 AM
enabled: true,
});
// Security Scan
const securityScan = new plugins.taskbuffer.Task({
name: 'security-scan',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('security-scan');
try {
await execution?.addLog('Starting security scan...', 'info');
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Aborting security scan...', 'warning');
return;
}
const vulnerabilities = [];
// Check for exposed ports
await execution?.addLog('Checking for exposed ports...', 'info');
// TODO: Actual port scanning logic
// Check for outdated images
await execution?.addLog('Checking for outdated images...', 'info');
const images = await taskManager.cloudlyRef.imageManager.CImage.getInstances({});
for (const image of images) {
if (execution && (taskManager.isCancellationRequested(execution.id) || execution.data.status === 'cancelled')) {
await execution.addLog('Cancellation requested. Stopping loop...', 'warning');
break;
}
// TODO: Check if image is outdated
const isOutdated = Math.random() > 0.7; // 30% outdated for demo
if (isOutdated) {
vulnerabilities.push({
type: 'outdated-image',
severity: 'medium',
image: image.data.name,
version: image.data.version,
});
}
}
// Check for weak passwords
await execution?.addLog('Checking for weak configurations...', 'info');
// TODO: Configuration checks
await execution?.setMetric('vulnerabilitiesFound', vulnerabilities.length);
await execution?.setMetric('vulnerabilities', vulnerabilities);
const severity = vulnerabilities.length > 0 ? 'warning' : 'success';
await execution?.addLog(
`Security scan completed: ${vulnerabilities.length} vulnerabilities found`,
severity as any
);
return { vulnerabilities };
} catch (error) {
await execution?.addLog(`Security scan error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('security-scan', securityScan, {
description: 'Run security checks on services',
category: 'security',
schedule: '0 1 * * *', // Daily at 1 AM
enabled: true,
});
// Docker Cleanup
const dockerCleanup = new plugins.taskbuffer.Task({
name: 'docker-cleanup',
taskFunction: async () => {
const execution = taskManager.getCurrentExecution('docker-cleanup');
try {
await execution?.addLog('Starting Docker cleanup...', 'info');
// TODO: Implement actual Docker cleanup
await execution?.addLog('Removing stopped containers...', 'info');
const removedContainers = 0; // Placeholder
await execution?.addLog('Removing unused images...', 'info');
const removedImages = 0; // Placeholder
await execution?.addLog('Removing unused volumes...', 'info');
const removedVolumes = 0; // Placeholder
await execution?.addLog('Removing unused networks...', 'info');
const removedNetworks = 0; // Placeholder
await execution?.setMetric('removedContainers', removedContainers);
await execution?.setMetric('removedImages', removedImages);
await execution?.setMetric('removedVolumes', removedVolumes);
await execution?.setMetric('removedNetworks', removedNetworks);
await execution?.addLog(
`Docker cleanup completed: ${removedContainers} containers, ${removedImages} images, ${removedVolumes} volumes, ${removedNetworks} networks removed`,
'success'
);
return {
containers: removedContainers,
images: removedImages,
volumes: removedVolumes,
networks: removedNetworks,
};
} catch (error) {
await execution?.addLog(`Docker cleanup error: ${error.message}`, 'error');
throw error;
}
},
});
taskManager.registerTask('docker-cleanup', dockerCleanup, {
description: 'Remove unused Docker images and containers',
category: 'cleanup',
schedule: '0 5 * * *', // Daily at 5 AM
enabled: true,
});
logger.log('info', 'Predefined tasks registered successfully');
}

View File

@@ -1,35 +0,0 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { logger } from '../logger.js';
export class CloudlyTaskmanager {
public cloudlyRef: Cloudly;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
public everyMinuteTask = new plugins.taskbuffer.Task({
taskFunction: async () => {},
});
public everyHourTask = new plugins.taskbuffer.Task({
taskFunction: async () => {
logger.log('info', `Performing hourly maintenance check.`);
const configs = await this.cloudlyRef.clusterManager.getAllClusters();
logger.log('info', `Got ${configs.length} configs`);
configs.map((configArg) => {
console.log(configArg.name);
});
},
});
public everyDayTask = new plugins.taskbuffer.Task({
taskFunction: async () => {},
});
public everyWeekTask = new plugins.taskbuffer.Task({
taskFunction: async () => {},
});
}

View File

@@ -54,6 +54,21 @@ export class CloudlyApiClient {
); );
} }
// Helper: resolve HTTP typedrequest endpoint
private get httpEndpoint() {
const base = (this.cloudlyUrl || '').replace(/\/$/, '');
return `${base}/typedrequest`;
}
// Helper: choose transport (WS if available, else HTTP)
private createWsRequest<T extends plugins.typedRequestInterfaces.ITypedRequest>(operation: string) {
return this.typedsocketClient?.createTypedRequest<T>(operation);
}
private createHttpRequest<T extends plugins.typedRequestInterfaces.ITypedRequest>(operation: string) {
return new plugins.typedrequest.TypedRequest<T>(this.httpEndpoint, operation);
}
public async start() { public async start() {
this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient( this.typedsocketClient = await plugins.typedsocket.TypedSocket.createClient(
this.typedrouter, this.typedrouter,
@@ -165,14 +180,54 @@ export class CloudlyApiClient {
getRegistryById: async (registryNameArg: string) => { getRegistryById: async (registryNameArg: string) => {
return ExternalRegistry.getExternalRegistryById(this, registryNameArg); return ExternalRegistry.getExternalRegistryById(this, registryNameArg);
}, },
updateRegistry: async (registryId: string, registryData: plugins.servezoneInterfaces.data.IExternalRegistry['data']): Promise<{ resultRegistry: plugins.servezoneInterfaces.data.IExternalRegistry }> => {
const op = 'updateExternalRegistry';
const payload = { identity: this.identity, registryId, registryData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_UpdateRegistry>(op).fire(payload);
},
deleteRegistry: async (registryId: string): Promise<{ ok: boolean }> => {
const op = 'deleteExternalRegistryById';
const payload = { identity: this.identity, registryId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_DeleteRegistryById>(op).fire(payload);
},
getRegistries: async () => { getRegistries: async () => {
return ExternalRegistry.getExternalRegistries(this); return ExternalRegistry.getExternalRegistries(this);
}, },
createRegistry: async (optionsArg: Parameters<typeof ExternalRegistry.createExternalRegistry>[1]) => { createRegistry: async (optionsArg: Parameters<typeof ExternalRegistry.createExternalRegistry>[1]) => {
return ExternalRegistry.createExternalRegistry(this, optionsArg); return ExternalRegistry.createExternalRegistry(this, optionsArg);
},
verifyRegistry: async (registryId: string): Promise<{ success: boolean; message: string; registry?: ExternalRegistry }> => {
const op = 'verifyExternalRegistry';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>(op);
const payload = { identity: this.identity, registryId } as any;
const resp = wsReq ? await wsReq.fire(payload) : await this.createHttpRequest<plugins.servezoneInterfaces.requests.externalRegistry.IReq_VerifyRegistry>(op).fire(payload);
let registryInstance: ExternalRegistry | undefined;
if (resp.registry) {
registryInstance = new ExternalRegistry(this);
Object.assign(registryInstance, resp.registry);
}
return { success: resp.success, message: resp.message, registry: registryInstance };
} }
} }
// Auth helpers
public async loginWithUsernameAndPassword(username: string, password: string): Promise<plugins.servezoneInterfaces.data.IIdentity> {
const op = 'adminLoginWithUsernameAndPassword';
// Login endpoint is exposed via HTTP typedrequest
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.admin.IReq_Admin_LoginWithUsernameAndPassword>(op);
const response = await httpReq.fire({ username, password });
this.identity = response.identity;
// If WS connection is available, tag it with identity for server-side guards
if (this.typedsocketClient) {
try { this.typedsocketClient.addTag('identity', this.identity); } catch {}
}
return this.identity;
}
public image = { public image = {
// Images // Images
getImageById: async (imageIdArg: string) => { getImageById: async (imageIdArg: string) => {
@@ -183,6 +238,13 @@ export class CloudlyApiClient {
}, },
createImage: async (optionsArg: Parameters<typeof Image.createImage>[1]) => { createImage: async (optionsArg: Parameters<typeof Image.createImage>[1]) => {
return Image.createImage(this, optionsArg); return Image.createImage(this, optionsArg);
},
deleteImage: async (imageId: string): Promise<void> => {
const op = 'deleteImage';
const payload = { identity: this.identity, imageId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(op);
if (wsReq) { await wsReq.fire(payload); return; }
await this.createHttpRequest<plugins.servezoneInterfaces.requests.image.IRequest_DeleteImage>(op).fire(payload);
} }
} }
@@ -196,6 +258,20 @@ export class CloudlyApiClient {
}, },
createService: async (optionsArg: Parameters<typeof Service.createService>[1]) => { createService: async (optionsArg: Parameters<typeof Service.createService>[1]) => {
return Service.createService(this, optionsArg); return Service.createService(this, optionsArg);
},
updateService: async (serviceId: string, serviceData: plugins.servezoneInterfaces.data.IService['data']): Promise<{ service: plugins.servezoneInterfaces.data.IService }> => {
const op = 'updateService';
const payload = { identity: this.identity, serviceId, serviceData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_UpdateService>(op).fire(payload);
},
deleteService: async (serviceId: string): Promise<void> => {
const op = 'deleteServiceById';
const payload = { identity: this.identity, serviceId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(op);
if (wsReq) { await wsReq.fire(payload); return; }
await this.createHttpRequest<plugins.servezoneInterfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>(op).fire(payload);
} }
} }
@@ -209,6 +285,14 @@ export class CloudlyApiClient {
}, },
createCluster: async (optionsArg: Parameters<typeof Cluster.createCluster>[1]) => { createCluster: async (optionsArg: Parameters<typeof Cluster.createCluster>[1]) => {
return Cluster.createCluster(this, optionsArg); return Cluster.createCluster(this, optionsArg);
},
createClusterAdvanced: async (clusterName: string, setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean') => {
const op = 'createCluster';
const payload: any = { identity: this.identity, clusterName };
if (setupMode) payload.setupMode = setupMode;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(op).fire(payload);
} }
} }
@@ -222,6 +306,13 @@ export class CloudlyApiClient {
}, },
createSecretBundle: async (optionsArg: Parameters<typeof SecretBundle.createSecretBundle>[1]) => { createSecretBundle: async (optionsArg: Parameters<typeof SecretBundle.createSecretBundle>[1]) => {
return SecretBundle.createSecretBundle(this, optionsArg); return SecretBundle.createSecretBundle(this, optionsArg);
},
deleteSecretBundleById: async (secretBundleId: string): Promise<{ ok: boolean }> => {
const op = 'deleteSecretBundleById';
const payload = { identity: this.identity, secretBundleId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.secretbundle.IReq_DeleteSecretBundleById>(op).fire(payload);
} }
} }
@@ -235,6 +326,260 @@ export class CloudlyApiClient {
}, },
createSecretGroup: async (optionsArg: Parameters<typeof SecretGroup.createSecretGroup>[1]) => { createSecretGroup: async (optionsArg: Parameters<typeof SecretGroup.createSecretGroup>[1]) => {
return SecretGroup.createSecretGroup(this, optionsArg); return SecretGroup.createSecretGroup(this, optionsArg);
},
deleteSecretGroupById: async (secretGroupId: string): Promise<{ ok: boolean }> => {
const op = 'deleteSecretGroupById';
const payload = { identity: this.identity, secretGroupId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.secretgroup.IReq_DeleteSecretGroupById>(op).fire(payload);
} }
} }
// Settings API
public settings = {
getSettings: async (): Promise<{
settings: plugins.servezoneInterfaces.data.ICloudlySettingsMasked
}> => {
const op = 'getSettings';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.settings.IRequest_GetSettings>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.settings.IRequest_GetSettings>(op);
return httpReq.fire({ identity: this.identity });
},
updateSettings: async (updates: Partial<plugins.servezoneInterfaces.data.ICloudlySettings>): Promise<{
success: boolean;
message: string;
}> => {
const op = 'updateSettings';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity, updates });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(op);
return httpReq.fire({ identity: this.identity, updates });
},
testProviderConnection: async (provider: string): Promise<{
connectionValid: boolean;
message: string;
}> => {
const op = 'testProviderConnection';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity, provider: provider as any });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(op);
return httpReq.fire({ identity: this.identity, provider: provider as any });
}
}
// Task API
public tasks = {
getTasks: async (): Promise<{
tasks: Array<{
name: string;
description: string;
category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
schedule?: string;
lastRun?: number;
enabled: boolean;
}>
}> => {
const op = 'getTasks';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>(op);
return httpReq.fire({ identity: this.identity });
},
getTaskExecutions: async (filter?: any): Promise<{
executions: plugins.servezoneInterfaces.data.ITaskExecution[];
}> => {
const op = 'getTaskExecutions';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity, filter });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>(op);
return httpReq.fire({ identity: this.identity, filter });
},
getTaskExecutionById: async (executionId: string): Promise<{
execution: plugins.servezoneInterfaces.data.ITaskExecution
}> => {
const op = 'getTaskExecutionById';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity, executionId });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>(op);
return httpReq.fire({ identity: this.identity, executionId });
},
triggerTask: async (taskName: string, userId?: string): Promise<{
execution: plugins.servezoneInterfaces.data.ITaskExecution
}> => {
const op = 'triggerTask';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity, taskName, userId });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>(op);
return httpReq.fire({ identity: this.identity, taskName, userId });
},
cancelTask: async (executionId: string): Promise<{ success: boolean }> => {
const op = 'cancelTask';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>(op);
if (wsReq) {
return wsReq.fire({ identity: this.identity, executionId });
}
const httpReq = this.createHttpRequest<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>(op);
return httpReq.fire({ identity: this.identity, executionId });
}
}
// Domain API
public domains = {
getDomains: async (): Promise<{ domains: plugins.servezoneInterfaces.data.IDomain[] }> => {
const op = 'getDomains';
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(op);
if (wsReq) return wsReq.fire({ identity: this.identity });
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomains>(op).fire({ identity: this.identity });
},
getDomainById: async (domainId: string): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain }> => {
const op = 'getDomainById';
const payload = { identity: this.identity, domainId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_GetDomainById>(op).fire(payload);
},
createDomain: async (domainData: plugins.servezoneInterfaces.data.IDomain['data']): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain }> => {
const op = 'createDomain';
const payload = { identity: this.identity, domainData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_CreateDomain>(op).fire(payload);
},
updateDomain: async (domainId: string, domainData: Partial<plugins.servezoneInterfaces.data.IDomain['data']>): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain }> => {
const op = 'updateDomain';
const payload = { identity: this.identity, domainId, domainData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_UpdateDomain>(op).fire(payload);
},
deleteDomain: async (domainId: string): Promise<{ success: boolean }> => {
const op = 'deleteDomain';
const payload = { identity: this.identity, domainId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_DeleteDomain>(op).fire(payload);
},
verifyDomain: async (domainId: string, verificationMethod?: 'dns' | 'http' | 'email' | 'manual'): Promise<{ domain: plugins.servezoneInterfaces.data.IDomain; verificationResult: any }> => {
const op = 'verifyDomain';
const payload = { identity: this.identity, domainId, verificationMethod } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.domain.IRequest_Any_Cloudly_VerifyDomain>(op).fire(payload);
},
};
// DNS API
public dns = {
getDnsEntries: async (zone?: string): Promise<{ dnsEntries: plugins.servezoneInterfaces.data.IDnsEntry[] }> => {
const op = 'getDnsEntries';
const payload = { identity: this.identity, zone } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntries>(op).fire(payload);
},
getDnsEntryById: async (dnsEntryId: string): Promise<{ dnsEntry: plugins.servezoneInterfaces.data.IDnsEntry }> => {
const op = 'getDnsEntryById';
const payload = { identity: this.identity, dnsEntryId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsEntryById>(op).fire(payload);
},
createDnsEntry: async (dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']): Promise<{ dnsEntry: plugins.servezoneInterfaces.data.IDnsEntry }> => {
const op = 'createDnsEntry';
const payload = { identity: this.identity, dnsEntryData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_CreateDnsEntry>(op).fire(payload);
},
updateDnsEntry: async (dnsEntryId: string, dnsEntryData: plugins.servezoneInterfaces.data.IDnsEntry['data']): Promise<{ dnsEntry: plugins.servezoneInterfaces.data.IDnsEntry }> => {
const op = 'updateDnsEntry';
const payload = { identity: this.identity, dnsEntryId, dnsEntryData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_UpdateDnsEntry>(op).fire(payload);
},
deleteDnsEntry: async (dnsEntryId: string): Promise<{ success: boolean }> => {
const op = 'deleteDnsEntry';
const payload = { identity: this.identity, dnsEntryId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_DeleteDnsEntry>(op).fire(payload);
},
getDnsZones: async (): Promise<{ zones: string[] }> => {
const op = 'getDnsZones';
const payload = { identity: this.identity } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.dns.IRequest_Any_Cloudly_GetDnsZones>(op).fire(payload);
},
};
// Deployment API
public deployments = {
getDeployments: async (): Promise<{ deployments: plugins.servezoneInterfaces.data.IDeployment[] }> => {
const op = 'getDeployments';
const payload = { identity: this.identity } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>(op).fire(payload);
},
getDeploymentById: async (deploymentId: string): Promise<{ deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
const op = 'getDeploymentById';
const payload = { identity: this.identity, deploymentId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_GetDeploymentById>(op).fire(payload);
},
createDeployment: async (deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>): Promise<{ deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
const op = 'createDeployment';
const payload = { identity: this.identity, deploymentData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>(op).fire(payload);
},
updateDeployment: async (deploymentId: string, deploymentData: Partial<plugins.servezoneInterfaces.data.IDeployment>): Promise<{ deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
const op = 'updateDeployment';
const payload = { identity: this.identity, deploymentId, deploymentData } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>(op).fire(payload);
},
deleteDeployment: async (deploymentId: string): Promise<{ success: boolean }> => {
const op = 'deleteDeploymentById';
const payload = { identity: this.identity, deploymentId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>(op).fire(payload);
},
restartDeployment: async (deploymentId: string): Promise<{ success: boolean; deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
const op = 'restartDeployment';
const payload = { identity: this.identity, deploymentId } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_RestartDeployment>(op).fire(payload);
},
scaleDeployment: async (deploymentId: string, replicas: number): Promise<{ success: boolean; deployment: plugins.servezoneInterfaces.data.IDeployment }> => {
const op = 'scaleDeployment';
const payload = { identity: this.identity, deploymentId, replicas } as any;
const wsReq = this.createWsRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(op);
if (wsReq) return wsReq.fire(payload);
return this.createHttpRequest<plugins.servezoneInterfaces.requests.deployment.IReq_Any_Cloudly_ScaleDeployment>(op).fire(payload);
},
};
} }

View File

@@ -21,10 +21,12 @@ export {
// @api.global scope // @api.global scope
import * as typedrequest from '@api.global/typedrequest'; import * as typedrequest from '@api.global/typedrequest';
import * as typedsocket from '@api.global/typedsocket'; import * as typedsocket from '@api.global/typedsocket';
import * as typedRequestInterfaces from '@api.global/typedrequest-interfaces';
export { export {
typedrequest, typedrequest,
typedsocket typedsocket,
typedRequestInterfaces,
} }
// @tsclass scope // @tsclass scope

View File

@@ -1,11 +1,28 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { CliClient } from "./classes.cliclient.js"; import { CliClient } from './classes.cliclient.js';
export const runCli = async () => { export const runCli = async () => {
const cliQenv = new plugins.qenv.Qenv(); const cliQenv = new plugins.qenv.Qenv();
const cloudlyUrl = await cliQenv.getEnvVarOnDemand('CLOUDLY_URL');
const token = process.env.CLOUDLY_TOKEN;
const username = process.env.CLOUDLY_USERNAME;
const password = process.env.CLOUDLY_PASSWORD;
const apiClient = new plugins.servezoneApi.CloudlyApiClient({ const apiClient = new plugins.servezoneApi.CloudlyApiClient({
registerAs: 'cli', registerAs: 'cli',
cloudlyUrl: await cliQenv.getEnvVarOnDemand('CLOUDLY_URL'), cloudlyUrl,
}); });
await apiClient.start();
if (token) {
await apiClient.getIdentityByToken(token, { tagConnection: true, statefullIdentity: true });
} else if (username && password) {
await apiClient.loginWithUsernameAndPassword(username, password);
} else {
console.log('No credentials provided. Set CLOUDLY_TOKEN or CLOUDLY_USERNAME/CLOUDLY_PASSWORD.');
}
const cliClient = new CliClient(apiClient); const cliClient = new CliClient(apiClient);
// Default action example: list clusters when invoked without subcommands
await cliClient.getClusters();
}; };

100
ts_interfaces/data/dns.ts Normal file
View File

@@ -0,0 +1,100 @@
export type TDnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA' | 'SRV' | 'CAA' | 'PTR';
export interface IDnsEntry {
id: string;
data: {
/**
* The DNS record type
*/
type: TDnsRecordType;
/**
* The DNS record name (e.g., www, @, mail)
* @ represents the root domain
*/
name: string;
/**
* The value of the DNS record
* - For A/AAAA: IP address
* - For CNAME: Target domain
* - For MX: Mail server hostname
* - For TXT: Text value
* - For NS: Nameserver hostname
* - For SRV: Target hostname
* - For CAA: CAA record value
* - For PTR: Domain name
*/
value: string;
/**
* Time to live in seconds
* Default: 3600 (1 hour)
*/
ttl: number;
/**
* Priority (used for MX and SRV records)
* Lower values have higher priority
*/
priority?: number;
/**
* The DNS zone this entry belongs to
* e.g., example.com
* @deprecated Use domainId instead
*/
zone: string;
/**
* The domain ID this DNS entry belongs to
* Links to the Domain entity
*/
domainId?: string;
/**
* Additional fields for SRV records
*/
weight?: number;
port?: number;
/**
* Whether this DNS entry is active
*/
active: boolean;
/**
* Optional description for documentation
*/
description?: string;
/**
* Timestamp when the entry was created
*/
createdAt?: number;
/**
* Timestamp when the entry was last updated
*/
updatedAt?: number;
/**
* Whether this DNS entry was auto-generated
*/
isAutoGenerated?: boolean;
/**
* The service ID that created this DNS entry (for auto-generated entries)
*/
sourceServiceId?: string;
/**
* The source type of this DNS entry
* - manual: Created by user through UI/API
* - service: Auto-generated from service configuration
* - system: Created by system processes
* - external: Synced from external DNS providers
*/
sourceType?: 'manual' | 'service' | 'system' | 'external';
};
}

View File

@@ -0,0 +1,124 @@
export type TDomainStatus = 'active' | 'pending' | 'expired' | 'suspended' | 'transferred';
export type TDomainVerificationStatus = 'verified' | 'pending' | 'failed' | 'not_required';
export interface IDomain {
id: string;
data: {
/**
* The domain name (e.g., example.com)
*/
name: string;
/**
* Description or notes about the domain
*/
description?: string;
/**
* Current status of the domain
*/
status: TDomainStatus;
/**
* Domain verification status
*/
verificationStatus: TDomainVerificationStatus;
/**
* Nameservers for the domain
*/
nameservers: string[];
/**
* Domain registrar information
*/
registrar?: {
name: string;
url?: string;
};
/**
* Domain registration date (timestamp)
*/
registeredAt?: number;
/**
* Domain expiration date (timestamp)
*/
expiresAt?: number;
/**
* Whether auto-renewal is enabled
*/
autoRenew: boolean;
/**
* DNSSEC enabled
*/
dnssecEnabled?: boolean;
/**
* Tags for categorization
*/
tags?: string[];
/**
* Whether this domain is primary for the organization
*/
isPrimary?: boolean;
/**
* SSL certificate status
*/
sslStatus?: 'active' | 'pending' | 'expired' | 'none';
/**
* Cloudly activation state controls whether we actively manage DNS/certificates
* - available: discovered/imported, not actively managed
* - activated: actively managed (DNS edits allowed, certs considered)
* - ignored: explicitly ignored from management/automation
*/
activationState?: 'available' | 'activated' | 'ignored';
/**
* Last verification attempt timestamp
*/
lastVerificationAt?: number;
/**
* Verification method used
*/
verificationMethod?: 'dns' | 'http' | 'email' | 'manual';
/**
* Verification token (for DNS/HTTP verification)
*/
verificationToken?: string;
/**
* Cloudflare zone ID if managed by Cloudflare
*/
cloudflareZoneId?: string;
/**
* Sync metadata
*/
syncSource?: 'cloudflare' | 'manual' | null;
lastSyncAt?: number;
/**
* Whether domain is managed externally
*/
isExternal?: boolean;
/**
* Creation timestamp
*/
createdAt?: number;
/**
* Last update timestamp
*/
updatedAt?: number;
};
}

View File

@@ -3,10 +3,108 @@ import * as plugins from '../plugins.js';
export interface IExternalRegistry { export interface IExternalRegistry {
id: string; id: string;
data: { data: {
/**
* Registry type
*/
type: 'docker' | 'npm'; type: 'docker' | 'npm';
/**
* Human-readable name for the registry
*/
name: string; name: string;
/**
* Registry URL (e.g., https://registry.gitlab.com, docker.io)
*/
url: string; url: string;
username: string;
password: string; /**
* Username for authentication (optional for token-based or public registries)
*/
username?: string;
/**
* Password, access token, or API key for authentication (optional for public registries)
*/
password?: string;
/**
* Optional description
*/
description?: string;
/**
* Whether this is the default registry for its type
*/
isDefault?: boolean;
/**
* Authentication type
*/
authType?: 'none' | 'basic' | 'token' | 'oauth2';
/**
* Allow insecure registry connections (HTTP or self-signed certs)
*/
insecure?: boolean;
/**
* Optional namespace/organization for the registry
*/
namespace?: string;
/**
* Proxy configuration
*/
proxy?: {
http?: string;
https?: string;
noProxy?: string;
};
/**
* Registry-specific configuration
*/
config?: {
/**
* For Docker registries
*/
dockerConfig?: {
email?: string;
serverAddress?: string;
};
/**
* For npm registries
*/
npmConfig?: {
scope?: string;
alwaysAuth?: boolean;
};
};
/**
* Status of the registry connection
*/
status?: 'active' | 'inactive' | 'error' | 'unverified';
/**
* Last error message if status is 'error'
*/
lastError?: string;
/**
* Timestamp when the registry was last successfully verified
*/
lastVerified?: number;
/**
* Timestamp when the registry was created
*/
createdAt?: number;
/**
* Timestamp when the registry was last updated
*/
updatedAt?: number;
}; };
} }

View File

@@ -2,7 +2,9 @@ export * from './cloudlyconfig.js';
export * from './cluster.js'; export * from './cluster.js';
export * from './config.js'; export * from './config.js';
export * from './deployment.js'; export * from './deployment.js';
export * from './dns.js';
export * from './docker.js'; export * from './docker.js';
export * from './domain.js';
export * from './event.js'; export * from './event.js';
export * from './externalregistry.js'; export * from './externalregistry.js';
export * from './image.js'; export * from './image.js';
@@ -13,6 +15,7 @@ export * from './clusternode.js';
export * from './settings.js'; export * from './settings.js';
export * from './service.js'; export * from './service.js';
export * from './status.js'; export * from './status.js';
export * from './taskexecution.js';
export * from './traffic.js'; export * from './traffic.js';
export * from './user.js'; export * from './user.js';
export * from './version.js'; export * from './version.js';

View File

@@ -54,8 +54,22 @@ export interface IService {
}; };
resources?: IServiceRessources; resources?: IServiceRessources;
domains: { domains: {
/**
* Optional domain ID to specify which domain to use
* If not specified, will use the default domain or require manual configuration
*/
domainId?: string;
/**
* The subdomain name (e.g., 'api', 'www', '@' for root)
*/
name: string; name: string;
/**
* The port to expose (defaults to ports.web if not specified)
*/
port?: number; port?: number;
/**
* The protocol for this domain entry
*/
protocol?: 'http' | 'https' | 'ssh'; protocol?: 'http' | 'https' | 'ssh';
}[]; }[];
deploymentIds: string[]; deploymentIds: string[];

View File

@@ -0,0 +1,84 @@
/**
* Task execution tracking for the task management system
* Tasks themselves are hard-coded using @push.rocks/taskbuffer
* This interface tracks execution history and outcomes
*/
export interface ITaskExecution {
id: string;
data: {
/**
* Name of the task being executed
*/
taskName: string;
/**
* Optional description of what the task does
*/
taskDescription?: string;
/**
* Category for grouping tasks
*/
category?: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
/**
* Timestamp when the task started
*/
startedAt: number;
/**
* Timestamp when the task completed
*/
completedAt?: number;
/**
* Current status of the task execution
*/
status: 'running' | 'completed' | 'failed' | 'cancelled';
/**
* Duration in milliseconds
*/
duration?: number;
/**
* How the task was triggered
*/
triggeredBy: 'schedule' | 'manual' | 'system';
/**
* User ID if manually triggered
*/
userId?: string;
/**
* Execution logs
*/
logs: Array<{
timestamp: number;
message: string;
severity: 'info' | 'warning' | 'error' | 'success';
}>;
/**
* Task-specific metrics
*/
metrics?: {
[key: string]: any;
};
/**
* Final result/output of the task
*/
result?: any;
/**
* Error details if the task failed
*/
error?: {
message: string;
stack?: string;
code?: string;
};
};
}

View File

@@ -0,0 +1,93 @@
import * as plugins from '../plugins.js';
import type { IDnsEntry } from '../data/dns.js';
import type { IIdentity } from '../data/user.js';
export interface IRequest_Any_Cloudly_GetDnsEntries
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetDnsEntries
> {
method: 'getDnsEntries';
request: {
identity: IIdentity;
zone?: string; // Optional filter by zone
};
response: {
dnsEntries: IDnsEntry[];
};
}
export interface IRequest_Any_Cloudly_GetDnsEntryById
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetDnsEntryById
> {
method: 'getDnsEntryById';
request: {
identity: IIdentity;
dnsEntryId: string;
};
response: {
dnsEntry: IDnsEntry;
};
}
export interface IRequest_Any_Cloudly_CreateDnsEntry
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_CreateDnsEntry
> {
method: 'createDnsEntry';
request: {
identity: IIdentity;
dnsEntryData: IDnsEntry['data'];
};
response: {
dnsEntry: IDnsEntry;
};
}
export interface IRequest_Any_Cloudly_UpdateDnsEntry
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_UpdateDnsEntry
> {
method: 'updateDnsEntry';
request: {
identity: IIdentity;
dnsEntryId: string;
dnsEntryData: IDnsEntry['data'];
};
response: {
dnsEntry: IDnsEntry;
};
}
export interface IRequest_Any_Cloudly_DeleteDnsEntry
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_DeleteDnsEntry
> {
method: 'deleteDnsEntry';
request: {
identity: IIdentity;
dnsEntryId: string;
};
response: {
success: boolean;
};
}
export interface IRequest_Any_Cloudly_GetDnsZones
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetDnsZones
> {
method: 'getDnsZones';
request: {
identity: IIdentity;
};
response: {
zones: string[];
};
}

View File

@@ -0,0 +1,99 @@
import * as plugins from '../plugins.js';
import type { IDomain } from '../data/domain.js';
import type { IIdentity } from '../data/user.js';
export interface IRequest_Any_Cloudly_GetDomains
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetDomains
> {
method: 'getDomains';
request: {
identity: IIdentity;
};
response: {
domains: IDomain[];
};
}
export interface IRequest_Any_Cloudly_GetDomainById
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetDomainById
> {
method: 'getDomainById';
request: {
identity: IIdentity;
domainId: string;
};
response: {
domain: IDomain;
};
}
export interface IRequest_Any_Cloudly_CreateDomain
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_CreateDomain
> {
method: 'createDomain';
request: {
identity: IIdentity;
domainData: IDomain['data'];
};
response: {
domain: IDomain;
};
}
export interface IRequest_Any_Cloudly_UpdateDomain
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_UpdateDomain
> {
method: 'updateDomain';
request: {
identity: IIdentity;
domainId: string;
domainData: IDomain['data'];
};
response: {
domain: IDomain;
};
}
export interface IRequest_Any_Cloudly_DeleteDomain
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_DeleteDomain
> {
method: 'deleteDomain';
request: {
identity: IIdentity;
domainId: string;
};
response: {
success: boolean;
};
}
export interface IRequest_Any_Cloudly_VerifyDomain
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_VerifyDomain
> {
method: 'verifyDomain';
request: {
identity: IIdentity;
domainId: string;
verificationMethod?: 'dns' | 'http' | 'email' | 'manual';
};
response: {
domain: IDomain;
verificationResult: {
success: boolean;
message?: string;
details?: any;
};
};
}

View File

@@ -50,6 +50,7 @@ export interface IReq_UpdateRegistry extends plugins.typedrequestInterfaces.impl
method: 'updateExternalRegistry'; method: 'updateExternalRegistry';
request: { request: {
identity: userInterfaces.IIdentity; identity: userInterfaces.IIdentity;
registryId: string;
registryData: data.IExternalRegistry['data']; registryData: data.IExternalRegistry['data'];
}; };
response: { response: {
@@ -70,3 +71,19 @@ export interface IReq_DeleteRegistryById extends plugins.typedrequestInterfaces.
ok: boolean; ok: boolean;
}; };
} }
export interface IReq_VerifyRegistry extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_VerifyRegistry
> {
method: 'verifyExternalRegistry';
request: {
identity: userInterfaces.IIdentity;
registryId: string;
};
response: {
success: boolean;
message?: string;
registry?: data.IExternalRegistry;
};
}

View File

@@ -5,6 +5,9 @@ import * as baremetalRequests from './baremetal.js';
import * as certificateRequests from './certificate.js'; import * as certificateRequests from './certificate.js';
import * as clusterRequests from './cluster.js'; import * as clusterRequests from './cluster.js';
import * as configRequests from './config.js'; import * as configRequests from './config.js';
import * as deploymentRequests from './deployment.js';
import * as dnsRequests from './dns.js';
import * as domainRequests from './domain.js';
import * as externalRegistryRequests from './externalregistry.js'; import * as externalRegistryRequests from './externalregistry.js';
import * as identityRequests from './identity.js'; import * as identityRequests from './identity.js';
import * as imageRequests from './image.js'; import * as imageRequests from './image.js';
@@ -19,6 +22,7 @@ import * as serverRequests from './server.js';
import * as serviceRequests from './service.js'; import * as serviceRequests from './service.js';
import * as settingsRequests from './settings.js'; import * as settingsRequests from './settings.js';
import * as statusRequests from './status.js'; import * as statusRequests from './status.js';
import * as taskRequests from './task.js';
import * as versionRequests from './version.js'; import * as versionRequests from './version.js';
export { export {
@@ -27,6 +31,9 @@ export {
certificateRequests as certificate, certificateRequests as certificate,
clusterRequests as cluster, clusterRequests as cluster,
configRequests as config, configRequests as config,
deploymentRequests as deployment,
dnsRequests as dns,
domainRequests as domain,
externalRegistryRequests as externalRegistry, externalRegistryRequests as externalRegistry,
identityRequests as identity, identityRequests as identity,
imageRequests as image, imageRequests as image,
@@ -41,6 +48,7 @@ export {
serviceRequests as service, serviceRequests as service,
settingsRequests as settings, settingsRequests as settings,
statusRequests as status, statusRequests as status,
taskRequests as task,
versionRequests as version, versionRequests as version,
}; };

View File

@@ -0,0 +1,95 @@
import * as plugins from '../plugins.js';
import * as data from '../data/index.js';
import * as userInterfaces from '../data/user.js';
// Get all tasks
export interface IRequest_Any_Cloudly_GetTasks
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetTasks
> {
method: 'getTasks';
request: {
identity: userInterfaces.IIdentity;
};
response: {
tasks: Array<{
name: string;
description: string;
category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security';
schedule?: string;
lastRun?: number;
enabled: boolean;
}>;
};
}
// Get task executions
export interface IRequest_Any_Cloudly_GetTaskExecutions
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetTaskExecutions
> {
method: 'getTaskExecutions';
request: {
identity: userInterfaces.IIdentity;
filter?: {
taskName?: string;
status?: string;
startedAfter?: number;
startedBefore?: number;
};
};
response: {
executions: data.ITaskExecution[];
};
}
// Get task execution by ID
export interface IRequest_Any_Cloudly_GetTaskExecutionById
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_GetTaskExecutionById
> {
method: 'getTaskExecutionById';
request: {
identity: userInterfaces.IIdentity;
executionId: string;
};
response: {
execution: data.ITaskExecution;
};
}
// Trigger task manually
export interface IRequest_Any_Cloudly_TriggerTask
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_TriggerTask
> {
method: 'triggerTask';
request: {
identity: userInterfaces.IIdentity;
taskName: string;
userId?: string;
};
response: {
execution: data.ITaskExecution;
};
}
// Cancel a running task
export interface IRequest_Any_Cloudly_CancelTask
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IRequest_Any_Cloudly_CancelTask
> {
method: 'cancelTask';
request: {
identity: userInterfaces.IIdentity;
executionId: string;
};
response: {
success: boolean;
};
}

View File

@@ -14,24 +14,27 @@ export const loginStatePart: plugins.smartstate.StatePart<unknown, ILoginState>
export const loginAction = loginStatePart.createAction<{ username: string; password: string }>( export const loginAction = loginStatePart.createAction<{ username: string; password: string }>(
async (statePartArg, payloadArg) => { async (statePartArg, payloadArg) => {
const currentState = statePartArg.getState(); const currentState = statePartArg.getState();
const trLogin = let identity: plugins.interfaces.data.IIdentity = null;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.admin.IReq_Admin_LoginWithUsernameAndPassword>( try {
'/typedrequest', identity = await apiClient.loginWithUsernameAndPassword(payloadArg.username, payloadArg.password);
'adminLoginWithUsernameAndPassword' } catch (err) {
);
const response = await trLogin.fire({
username: payloadArg.username,
password: payloadArg.password,
}).catch(err => {
console.log(err); console.log(err);
return { }
...statePartArg.getState(), const newState = {
}
});
return {
...currentState, ...currentState,
...(response.identity ? { identity: response.identity } : {}), ...(identity ? { identity } : {}),
}; };
try {
// Keep shared API client in sync and establish WS for modules using sockets
apiClient.identity = identity || null;
if (apiClient.identity) {
if (!apiClient['typedsocketClient']) {
await apiClient.start();
}
try { apiClient.typedsocketClient.addTag('identity', apiClient.identity); } catch {}
}
} catch {}
return newState;
} }
); );
@@ -47,10 +50,14 @@ export interface IDataState {
secretGroups?: plugins.interfaces.data.ISecretGroup[]; secretGroups?: plugins.interfaces.data.ISecretGroup[];
secretBundles?: plugins.interfaces.data.ISecretBundle[]; secretBundles?: plugins.interfaces.data.ISecretBundle[];
clusters?: plugins.interfaces.data.ICluster[]; clusters?: plugins.interfaces.data.ICluster[];
externalRegistries?: plugins.interfaces.data.IExternalRegistry[];
images?: any[]; images?: any[];
services?: plugins.interfaces.data.IService[]; services?: plugins.interfaces.data.IService[];
deployments?: plugins.interfaces.data.IDeployment[]; deployments?: plugins.interfaces.data.IDeployment[];
dns?: any[]; domains?: plugins.interfaces.data.IDomain[];
dnsEntries?: plugins.interfaces.data.IDnsEntry[];
tasks?: any[];
taskExecutions?: plugins.interfaces.data.ITaskExecution[];
mails?: any[]; mails?: any[];
logs?: any[]; logs?: any[];
s3?: any[]; s3?: any[];
@@ -63,10 +70,14 @@ export const dataState = await appstate.getStatePart<IDataState>(
secretGroups: [], secretGroups: [],
secretBundles: [], secretBundles: [],
clusters: [], clusters: [],
externalRegistries: [],
images: [], images: [],
services: [], services: [],
deployments: [], deployments: [],
dns: [], domains: [],
dnsEntries: [],
tasks: [],
taskExecutions: [],
mails: [], mails: [],
logs: [], logs: [],
s3: [], s3: [],
@@ -76,93 +87,158 @@ export const dataState = await appstate.getStatePart<IDataState>(
'soft' 'soft'
); );
// Shared API client instance (used by UI actions)
export const apiClient = new plugins.servezoneApi.CloudlyApiClient({
registerAs: 'api',
cloudlyUrl: (typeof window !== 'undefined' && window.location?.origin) ? window.location.origin : undefined,
});
// Getting data // Getting data
export const getAllDataAction = dataState.createAction(async (statePartArg) => { export const getAllDataAction = dataState.createAction(async (statePartArg) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
// SecretsGroups // SecretsGroups
const trGetSecretGroups = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretgroup.IReq_GetSecretGroups>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', const secretGroups = await apiClient.secretgroup.getSecretGroups();
'getSecretGroups' currentState = {
); ...currentState,
const response = await trGetSecretGroups.fire({ secretGroups: secretGroups,
identity: loginStatePart.getState().identity, };
}); } catch (err) {
currentState = { console.error('Failed to fetch secret groups:', err);
...currentState, currentState = {
secretGroups: response.secretGroups, ...currentState,
}; secretGroups: [],
};
}
// SecretBundles // SecretBundles
const trGetSecretBundles = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretbundle.IReq_GetSecretBundles>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', const responseSecretBundles = await apiClient.secretbundle.getSecretBundles();
'getSecretBundles' currentState = {
); ...currentState,
const responseSecretBundles = await trGetSecretBundles.fire({ secretBundles: responseSecretBundles,
identity: loginStatePart.getState().identity, };
}); } catch (err) {
currentState = { console.error('Failed to fetch secret bundles:', err);
...currentState, currentState = {
secretBundles: responseSecretBundles.secretBundles, ...currentState,
}; secretBundles: [],
};
}
// images // images
const trGetImages = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_GetAllImages>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', const images = await apiClient.image.getImages();
'getAllImages' currentState = {
); ...currentState,
const responseImages = await trGetImages.fire({ images: images,
identity: loginStatePart.getState().identity, };
}); } catch (err) {
currentState = { console.error('Failed to fetch images:', err);
...currentState, currentState = {
images: responseImages.images, ...currentState,
}; images: [],
};
}
// Clusters // Clusters
const trGetClusters = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IReq_Any_Cloudly_GetClusters>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', const clusters = await apiClient.cluster.getClusters();
'getClusters' currentState = {
); ...currentState,
const responseClusters = await trGetClusters.fire({ clusters: clusters,
identity: loginStatePart.getState().identity, }
}); } catch (err) {
console.error('Failed to fetch clusters:', err);
currentState = {
...currentState,
clusters: [],
}
}
currentState = { // External Registries via shared API client
...currentState, try {
clusters: responseClusters.clusters, apiClient.identity = loginStatePart.getState().identity;
const registries = await apiClient.externalRegistry.getRegistries();
currentState = {
...currentState,
externalRegistries: registries,
};
} catch (error) {
console.error('Failed to fetch external registries:', error);
currentState = {
...currentState,
externalRegistries: [],
};
} }
// Services // Services
const trGetServices = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_GetServices>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', const services = await apiClient.services.getServices();
'getServices' currentState = {
); ...currentState,
const responseServices = await trGetServices.fire({ services: services,
identity: loginStatePart.getState().identity, };
}); } catch (error) {
currentState = { console.error('Failed to fetch services:', error);
...currentState, currentState = {
services: responseServices.services, ...currentState,
}; services: [],
};
}
// Deployments // Deployments
const trGetDeployments = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_GetDeployments>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', const responseDeployments = await apiClient.deployments.getDeployments();
'getDeployments' currentState = {
); ...currentState,
const responseDeployments = await trGetDeployments.fire({ deployments: responseDeployments?.deployments || [],
identity: loginStatePart.getState().identity, };
}); } catch (error) {
currentState = { console.error('Failed to fetch deployments:', error);
...currentState, currentState = {
deployments: responseDeployments.deployments, ...currentState,
}; deployments: [],
};
}
// Domains via API client
try {
apiClient.identity = loginStatePart.getState().identity;
const responseDomains = await apiClient.domains.getDomains();
currentState = {
...currentState,
domains: responseDomains?.domains || [],
};
} catch (error) {
console.error('Failed to fetch domains:', error);
currentState = {
...currentState,
domains: [],
};
}
// DNS Entries via API client
try {
apiClient.identity = loginStatePart.getState().identity;
const responseDnsEntries = await apiClient.dns.getDnsEntries();
currentState = {
...currentState,
dnsEntries: responseDnsEntries?.dnsEntries || [],
};
} catch (error) {
console.error('Failed to fetch DNS entries:', error);
currentState = {
...currentState,
dnsEntries: [],
};
}
return currentState; return currentState;
}); });
@@ -171,15 +247,8 @@ export const getAllDataAction = dataState.createAction(async (statePartArg) => {
export const createServiceAction = dataState.createAction( export const createServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => { async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trCreateService = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_CreateService>( await apiClient.services.createService(payloadArg.serviceData);
'/typedrequest',
'createService'
);
const response = await trCreateService.fire({
identity: loginStatePart.getState().identity,
serviceData: payloadArg.serviceData,
});
currentState = await dataState.dispatchAction(getAllDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
@@ -188,16 +257,8 @@ export const createServiceAction = dataState.createAction(
export const updateServiceAction = dataState.createAction( export const updateServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => { async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trUpdateService = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_UpdateService>( await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData);
'/typedrequest',
'updateService'
);
const response = await trUpdateService.fire({
identity: loginStatePart.getState().identity,
serviceId: payloadArg.serviceId,
serviceData: payloadArg.serviceData,
});
currentState = await dataState.dispatchAction(getAllDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
@@ -206,15 +267,8 @@ export const updateServiceAction = dataState.createAction(
export const deleteServiceAction = dataState.createAction( export const deleteServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceId: string }) => { async (statePartArg, payloadArg: { serviceId: string }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trDeleteService = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.service.IRequest_Any_Cloudly_DeleteServiceById>( await apiClient.services.deleteService(payloadArg.serviceId);
'/typedrequest',
'deleteServiceById'
);
const response = await trDeleteService.fire({
identity: loginStatePart.getState().identity,
serviceId: payloadArg.serviceId,
});
currentState = await dataState.dispatchAction(getAllDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
@@ -224,16 +278,13 @@ export const deleteServiceAction = dataState.createAction(
export const createSecretGroupAction = dataState.createAction( export const createSecretGroupAction = dataState.createAction(
async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => { async (statePartArg, payloadArg: plugins.interfaces.data.ISecretGroup) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trCreateSecretGroup = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretgroup.IReq_CreateSecretGroup>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', await apiClient.secretgroup.createSecretGroup(payloadArg.data);
'createSecretGroup' currentState = await dataState.dispatchAction(getAllDataAction, null);
); } catch (err) {
const response = await trCreateSecretGroup.fire({ console.error('Failed to create secret group:', err);
identity: loginStatePart.getState().identity, }
secretGroup: payloadArg,
});
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
return currentState; return currentState;
} }
@@ -242,16 +293,13 @@ export const createSecretGroupAction = dataState.createAction(
export const deleteSecretGroupAction = dataState.createAction( export const deleteSecretGroupAction = dataState.createAction(
async (statePartArg, payloadArg: { secretGroupId: string }) => { async (statePartArg, payloadArg: { secretGroupId: string }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trDeleteSecretGroup = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretgroup.IReq_DeleteSecretGroupById>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', await apiClient.secretgroup.deleteSecretGroupById(payloadArg.secretGroupId);
'deleteSecretGroupById' currentState = await dataState.dispatchAction(getAllDataAction, null);
); } catch (err) {
const response = await trDeleteSecretGroup.fire({ console.error('Failed to delete secret group:', err);
identity: loginStatePart.getState().identity, }
secretGroupId: payloadArg.secretGroupId,
});
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
); );
@@ -260,16 +308,13 @@ export const deleteSecretGroupAction = dataState.createAction(
export const deleteSecretBundleAction = dataState.createAction( export const deleteSecretBundleAction = dataState.createAction(
async (statePartArg, payloadArg: { configBundleId: string }) => { async (statePartArg, payloadArg: { configBundleId: string }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trDeleteConfigBundle = try {
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.secretbundle.IReq_DeleteSecretBundleById>( apiClient.identity = loginStatePart.getState().identity;
'/typedrequest', await apiClient.secretbundle.deleteSecretBundleById(payloadArg.configBundleId);
'deleteSecretBundleById' currentState = await dataState.dispatchAction(getAllDataAction, null);
); } catch (err) {
const response = await trDeleteConfigBundle.fire({ console.error('Failed to delete secret bundle:', err);
identity: loginStatePart.getState().identity, }
secretBundleId: payloadArg.configBundleId,
});
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
); );
@@ -278,22 +323,9 @@ export const deleteSecretBundleAction = dataState.createAction(
export const createImageAction = dataState.createAction( export const createImageAction = dataState.createAction(
async (statePartArg, payloadArg: { imageName: string, description: string }) => { async (statePartArg, payloadArg: { imageName: string, description: string }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trCreateImage = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_CreateImage>( await apiClient.image.createImage({ name: payloadArg.imageName, description: payloadArg.description });
'/typedrequest', currentState = await dataState.dispatchAction(getAllDataAction, null);
'createImage'
);
const response = await trCreateImage.fire({
identity: loginStatePart.getState().identity,
name: payloadArg.imageName,
description: payloadArg.description,
});
currentState = {
...currentState,
...{
images: [...currentState.images, response.image],
},
};
return currentState; return currentState;
} }
); );
@@ -301,21 +333,9 @@ export const createImageAction = dataState.createAction(
export const deleteImageAction = dataState.createAction( export const deleteImageAction = dataState.createAction(
async (statePartArg, payloadArg: { imageId: string }) => { async (statePartArg, payloadArg: { imageId: string }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trDeleteImage = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.image.IRequest_DeleteImage>( await apiClient.image.deleteImage(payloadArg.imageId);
'/typedrequest', currentState = await dataState.dispatchAction(getAllDataAction, null);
'deleteImage'
);
const response = await trDeleteImage.fire({
identity: loginStatePart.getState().identity,
imageId: payloadArg.imageId,
});
currentState = {
...currentState,
...{
images: currentState.images.filter((image) => image.id !== payloadArg.imageId),
},
};
return currentState; return currentState;
} }
); );
@@ -324,15 +344,8 @@ export const deleteImageAction = dataState.createAction(
export const createDeploymentAction = dataState.createAction( export const createDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => { async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trCreateDeployment = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_CreateDeployment>( await apiClient.deployments.createDeployment(payloadArg.deploymentData);
'/typedrequest',
'createDeployment'
);
const response = await trCreateDeployment.fire({
identity: loginStatePart.getState().identity,
deploymentData: payloadArg.deploymentData,
});
currentState = await dataState.dispatchAction(getAllDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
@@ -341,16 +354,8 @@ export const createDeploymentAction = dataState.createAction(
export const updateDeploymentAction = dataState.createAction( export const updateDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => { async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trUpdateDeployment = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_UpdateDeployment>( await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
'/typedrequest',
'updateDeployment'
);
const response = await trUpdateDeployment.fire({
identity: loginStatePart.getState().identity,
deploymentId: payloadArg.deploymentId,
deploymentData: payloadArg.deploymentData,
});
currentState = await dataState.dispatchAction(getAllDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
@@ -359,20 +364,206 @@ export const updateDeploymentAction = dataState.createAction(
export const deleteDeploymentAction = dataState.createAction( export const deleteDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentId: string }) => { async (statePartArg, payloadArg: { deploymentId: string }) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trDeleteDeployment = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.deployment.IReq_Any_Cloudly_DeleteDeploymentById>( await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
'/typedrequest',
'deleteDeploymentById'
);
const response = await trDeleteDeployment.fire({
identity: loginStatePart.getState().identity,
deploymentId: payloadArg.deploymentId,
});
currentState = await dataState.dispatchAction(getAllDataAction, null); currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState; return currentState;
} }
); );
// DNS Actions
export const createDnsEntryAction = dataState.createAction(
async (statePartArg, payloadArg: { dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.dns.createDnsEntry(payloadArg.dnsEntryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const updateDnsEntryAction = dataState.createAction(
async (statePartArg, payloadArg: { dnsEntryId: string; dnsEntryData: plugins.interfaces.data.IDnsEntry['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.dns.updateDnsEntry(payloadArg.dnsEntryId, payloadArg.dnsEntryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const deleteDnsEntryAction = dataState.createAction(
async (statePartArg, payloadArg: { dnsEntryId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.dns.deleteDnsEntry(payloadArg.dnsEntryId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
// Domain Actions
export const createDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainData: plugins.interfaces.data.IDomain['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.domains.createDomain(payloadArg.domainData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const updateDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainId: string; domainData: plugins.interfaces.data.IDomain['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.domains.updateDomain(payloadArg.domainId, payloadArg.domainData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const deleteDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.domains.deleteDomain(payloadArg.domainId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const verifyDomainAction = dataState.createAction(
async (statePartArg, payloadArg: { domainId: string; verificationMethod?: 'dns' | 'http' | 'email' | 'manual' }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.domains.verifyDomain(payloadArg.domainId, payloadArg.verificationMethod);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
// External Registry Actions
export const createExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.externalRegistry.createRegistry(payloadArg.registryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const updateExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryId: string; registryData: plugins.interfaces.data.IExternalRegistry['data'] }) => {
let currentState = statePartArg.getState();
try {
apiClient.identity = loginStatePart.getState().identity;
await apiClient.externalRegistry.updateRegistry(payloadArg.registryId, payloadArg.registryData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
} catch (err) {
console.error('Failed to update external registry:', err);
}
return currentState;
}
);
export const deleteExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryId: string }) => {
let currentState = statePartArg.getState();
try {
apiClient.identity = loginStatePart.getState().identity;
await apiClient.externalRegistry.deleteRegistry(payloadArg.registryId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
} catch (err) {
console.error('Failed to delete external registry:', err);
}
return currentState;
}
);
export const verifyExternalRegistryAction = dataState.createAction(
async (statePartArg, payloadArg: { registryId: string }) => {
let currentState = statePartArg.getState();
try {
apiClient.identity = loginStatePart.getState().identity;
const result = await apiClient.externalRegistry.verifyRegistry(payloadArg.registryId);
if (result.success && result.registry) {
const regs = (currentState.externalRegistries || []).slice();
const idx = regs.findIndex(r => r.id === payloadArg.registryId);
if (idx >= 0) {
// Preserve instance; update its data + shallow props
const instance: any = regs[idx];
instance.data = result.registry.data;
instance.id = result.registry.id;
regs[idx] = instance;
}
currentState = {
...currentState,
externalRegistries: regs,
};
}
} catch (err) {
console.error('Failed to verify external registry:', err);
}
return currentState;
}
);
// Task Actions
export const taskActions = {
getTasks: dataState.createAction(
async (statePartArg, payloadArg: {}) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const response = await apiClient.tasks.getTasks();
return {
...currentState,
tasks: response.tasks,
};
}
),
getTaskExecutions: dataState.createAction(
async (statePartArg, payloadArg: { filter?: any }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
const response = await apiClient.tasks.getTaskExecutions(payloadArg.filter);
return {
...currentState,
taskExecutions: response.executions,
};
}
),
getTaskExecutionById: dataState.createAction(
async (statePartArg, payloadArg: { executionId: string }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.tasks.getTaskExecutionById(payloadArg.executionId);
return currentState;
}
),
triggerTask: dataState.createAction(
async (statePartArg, payloadArg: { taskName: string; userId?: string }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.tasks.triggerTask(payloadArg.taskName, payloadArg.userId);
return currentState;
}
),
cancelTask: dataState.createAction(
async (statePartArg, payloadArg: { executionId: string }) => {
const currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.tasks.cancelTask(payloadArg.executionId);
return currentState;
}
),
};
// cluster // cluster
export const addClusterAction = dataState.createAction( export const addClusterAction = dataState.createAction(
async ( async (
@@ -383,21 +574,8 @@ export const addClusterAction = dataState.createAction(
} }
) => { ) => {
let currentState = statePartArg.getState(); let currentState = statePartArg.getState();
const trAddCluster = apiClient.identity = loginStatePart.getState().identity;
new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.cluster.IRequest_CreateCluster>( await apiClient.cluster.createClusterAdvanced(payloadArg.clusterName, payloadArg.setupMode);
'/typedrequest', return await dataState.dispatchAction(getAllDataAction, null);
'createCluster'
);
const response = await trAddCluster.fire({
identity: loginStatePart.getState().identity,
...payloadArg,
});
currentState = {
...currentState,
...{
clusters: [...currentState.clusters, response.cluster],
},
}
return currentState;
} }
); );

View File

@@ -11,21 +11,23 @@ import {
html, html,
state state
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import { CloudlyViewBackups } from './cloudly-view-backups.js'; import { CloudlyViewBackups } from './views/backups/index.js';
import { CloudlyViewClusters } from './cloudly-view-clusters.js'; import { CloudlyViewClusters } from './views/clusters/index.js';
import { CloudlyViewDbs } from './cloudly-view-dbs.js'; import { CloudlyViewDbs } from './views/dbs/index.js';
import { CloudlyViewDeployments } from './cloudly-view-deployments.js'; import { CloudlyViewDeployments } from './views/deployments/index.js';
import { CloudlyViewDns } from './cloudly-view-dns.js'; import { CloudlyViewDns } from './views/dns/index.js';
import { CloudlyViewImages } from './cloudly-view-images.js'; import { CloudlyViewDomains } from './views/domains/index.js';
import { CloudlyViewLogs } from './cloudly-view-logs.js'; import { CloudlyViewImages } from './views/images/index.js';
import { CloudlyViewMails } from './cloudly-view-mails.js'; import { CloudlyViewLogs } from './views/logs/index.js';
import { CloudlyViewOverview } from './cloudly-view-overview.js'; import { CloudlyViewMails } from './views/mails/index.js';
import { CloudlyViewS3 } from './cloudly-view-s3.js'; import { CloudlyViewOverview } from './views/overview/index.js';
import { CloudlyViewSecretBundles } from './cloudly-view-secretbundles.js'; import { CloudlyViewS3 } from './views/s3/index.js';
import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; import { CloudlyViewSecretBundles } from './views/secretbundles/index.js';
import { CloudlyViewServices } from './cloudly-view-services.js'; import { CloudlyViewSecretGroups } from './views/secretgroups/index.js';
import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; import { CloudlyViewServices } from './views/services/index.js';
import { CloudlyViewSettings } from './cloudly-view-settings.js'; import { CloudlyViewExternalRegistries } from './views/externalregistries/index.js';
import { CloudlyViewSettings } from './views/settings/index.js';
import { CloudlyViewTasks } from './views/tasks/index.js';
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
@@ -42,6 +44,105 @@ export class CloudlyDashboard extends DeesElement {
clusters: [], clusters: [],
}; };
// Keep view tabs stable across renders to preserve active selection
private readonly viewTabs: plugins.deesCatalog.IView[] = [
{
name: 'Overview',
iconName: 'lucide:LayoutDashboard',
element: CloudlyViewOverview,
},
{
name: 'Settings',
iconName: 'lucide:Settings',
element: CloudlyViewSettings,
},
{
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: 'Tasks',
iconName: 'lucide:ListChecks',
element: CloudlyViewTasks,
},
{
name: 'Domains',
iconName: 'lucide:Globe2',
element: CloudlyViewDomains,
},
{
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,
},
];
constructor() { constructor() {
super(); super();
document.title = `cloudly v${commitinfo.version}`; document.title = `cloudly v${commitinfo.version}`;
@@ -74,93 +175,7 @@ export class CloudlyDashboard extends DeesElement {
<div class="maincontainer"> <div class="maincontainer">
<dees-simple-login name="cloudly v${commitinfo.version}"> <dees-simple-login name="cloudly v${commitinfo.version}">
<dees-simple-appdash name="cloudly v${commitinfo.version}" <dees-simple-appdash name="cloudly v${commitinfo.version}"
.viewTabs=${[ .viewTabs=${this.viewTabs}
{
name: 'Overview',
iconName: 'lucide:LayoutDashboard',
element: CloudlyViewOverview,
},
{
name: 'Settings',
iconName: 'lucide:Settings',
element: CloudlyViewSettings,
},
{
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[]}
></dees-simple-appdash> ></dees-simple-appdash>
</dees-simple-login> </dees-simple-login>
</div> </div>
@@ -202,6 +217,13 @@ export class CloudlyDashboard extends DeesElement {
console.log(loginState); console.log(loginState);
if (loginState.identity) { if (loginState.identity) {
this.identity = loginState.identity; this.identity = loginState.identity;
try {
appstate.apiClient.identity = loginState.identity;
if (!appstate.apiClient['typedsocketClient']) {
await appstate.apiClient.start();
}
try { appstate.apiClient.typedsocketClient.addTag('identity', appstate.apiClient.identity); } catch {}
} catch (e) { console.warn('Failed to initialize API client WS', e); }
await simpleLogin.switchToSlottedContent(); await simpleLogin.switchToSlottedContent();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, null); await appstate.dataState.dispatchAction(appstate.getAllDataAction, null);
} }

View File

@@ -1,130 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-backups')
export class CloudlyViewBackups extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>Backups</cloudly-sectionheading>
<dees-table
.heading1=${'Backups'}
.heading2=${'decoded in client'}
.data=${this.data.backups}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,150 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-clusters')
export class CloudlyViewClusters extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
<dees-table
.heading1=${'Clusters'}
.heading2=${'decoded in client'}
.data=${this.data.clusters}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
console.log(itemArg);
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add cluster',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Cluster',
content: html`
<dees-form>
<dees-input-text
.key=${'clusterName'}
.label=${'cluster name'}
.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: [
{
name: 'create',
action: async (modalArg) => {
const data: {
clusterName: string;
setupMode: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
} = (await modalArg.shadowRoot
.querySelector('dees-form')
.collectFormData()) as any;
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
await modalArg.destroy();
},
},
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,130 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-dbs')
export class CloudlyViewDbs extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>DBs</cloudly-sectionheading>
<dees-table
.heading1=${'DBs'}
.heading2=${'decoded in client'}
.data=${this.data.dbs}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,352 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-deployments')
export class CloudlyViewDeployments extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subscription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subscription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 500;
}
.status-running {
background: #4caf50;
color: white;
}
.status-stopped {
background: #f44336;
color: white;
}
.status-paused {
background: #ff9800;
color: white;
}
.status-deploying {
background: #2196f3;
color: white;
}
.health-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
}
.health-healthy {
background: #e8f5e9;
color: #2e7d32;
}
.health-unhealthy {
background: #ffebee;
color: #c62828;
}
.health-unknown {
background: #f5f5f5;
color: #666;
}
.resource-usage {
display: flex;
gap: 12px;
font-size: 0.9em;
color: #888;
}
.resource-item {
display: flex;
align-items: center;
gap: 4px;
}
`,
];
private getServiceName(serviceId: string): string {
const service = this.data.services?.find(s => s.id === serviceId);
return service?.data?.name || serviceId;
}
private getNodeName(nodeId: string): string {
// This would ideally look up the cluster node name
// For now just return the ID shortened
return nodeId.substring(0, 8);
}
private getStatusBadgeHtml(status: string): any {
const className = `status-badge status-${status}`;
return html`<span class="${className}">${status}</span>`;
}
private getHealthIndicatorHtml(health?: string): any {
if (!health) health = 'unknown';
const className = `health-indicator health-${health}`;
const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?';
return html`<span class="${className}">${icon} ${health}</span>`;
}
private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any {
if (!deployment.resourceUsage) {
return html`<span style="color: #aaa;">N/A</span>`;
}
const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage;
return html`
<div class="resource-usage">
<div class="resource-item">
<lucide-icon name="Cpu" size="14"></lucide-icon>
${cpuUsagePercent?.toFixed(1) || 0}%
</div>
<div class="resource-item">
<lucide-icon name="MemoryStick" size="14"></lucide-icon>
${memoryUsedMB || 0} MB
</div>
</div>
`;
}
public render() {
return html`
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
<dees-table
.heading1=${'Deployments'}
.heading2=${'Service deployments running on cluster nodes'}
.data=${this.data.deployments || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => {
return {
Service: this.getServiceName(itemArg.serviceId),
Node: this.getNodeName(itemArg.nodeId),
Status: this.getStatusBadgeHtml(itemArg.status),
Health: this.getHealthIndicatorHtml(itemArg.healthStatus),
'Container ID': itemArg.containerId ?
html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` :
html`<span style="color: #aaa;">N/A</span>`,
Version: itemArg.version || 'latest',
'Resource Usage': this.getResourceUsageHtml(itemArg),
'Last Updated': itemArg.deployedAt ?
new Date(itemArg.deployedAt).toLocaleString() :
'Never',
};
}}
.dataActions=${[
{
name: 'Deploy Service',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const availableServices = this.data.services || [];
if (availableServices.length === 0) {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'No Services Available',
content: html`
<div style="text-align: center; padding: 24px;">
<lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon>
<div>Please create a service first before creating deployments.</div>
</div>
`,
menuOptions: [
{
name: 'OK',
action: async (modalArg) => {
await modalArg.destroy();
},
},
],
});
return;
}
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Deploy Service',
content: html`
<dees-form>
<dees-input-dropdown
.key=${'serviceId'}
.label=${'Service'}
.options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))}
.required=${true}>
</dees-input-dropdown>
<dees-input-text
.key=${'nodeId'}
.label=${'Target Node ID'}
.required=${true}
.description=${'Enter the cluster node ID where this service should be deployed'}>
</dees-input-text>
<dees-input-text
.key=${'version'}
.label=${'Version'}
.value=${'latest'}
.required=${true}>
</dees-input-text>
<dees-input-dropdown
.key=${'status'}
.label=${'Initial Status'}
.options=${['deploying', 'running']}
.value=${'deploying'}
.required=${true}>
</dees-input-dropdown>
</dees-form>
`,
menuOptions: [
{
name: 'Deploy',
action: async (modalArg) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.createDeploymentAction, {
deploymentData: {
serviceId: formData.serviceId,
nodeId: formData.nodeId,
status: formData.status,
version: formData.version,
deployedAt: Date.now(),
usedImageId: 'placeholder', // This would come from the service
deploymentLog: [],
},
});
await modalArg.destroy();
},
},
{
name: 'Cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'Restart',
iconName: 'refresh-cw',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Restart Deployment`,
content: html`
<div style="text-align:center">
Are you sure you want to restart this deployment?
</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold;">
${this.getServiceName(deployment.serviceId)}
</div>
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
Node: ${this.getNodeName(deployment.nodeId)}
</div>
</div>
`,
menuOptions: [
{
name: 'Cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Restart',
action: async (modalArg) => {
// TODO: Implement restart action
console.log('Restart deployment:', deployment);
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'Stop',
iconName: 'square',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, {
deploymentId: deployment.id,
deploymentData: {
...deployment,
status: 'stopped',
},
});
},
},
{
name: 'Delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Deployment`,
content: html`
<div style="text-align:center">
Are you sure you want to delete this deployment?
</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold;">
${this.getServiceName(deployment.serviceId)}
</div>
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">
Node: ${this.getNodeName(deployment.nodeId)}
</div>
<div style="color: #f44336; margin-top: 8px;">
This action cannot be undone.
</div>
</div>
`,
menuOptions: [
{
name: 'Cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Delete',
action: async (modalArg) => {
await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, {
deploymentId: deployment.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,129 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-dns')
export class CloudlyViewDns extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>DNS</cloudly-sectionheading>
<dees-table
.heading1=${'DNS'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,129 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-externalregistries')
export class CloudlyViewExternalRegistries extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
<dees-table
.heading1=${'External Registries'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,295 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-images')
export class CloudlyViewImages extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>Images</cloudly-sectionheading>
<dees-table
heading1="Images"
heading2="an image is needed for running a service"
.data=${this.data.images}
.displayFunction=${(image: plugins.interfaces.data.IImage) => {
return {
id: image.id,
name: image.data.name,
description: image.data.description,
versions: image.data.versions.length,
};
}}
.dataActions=${[
{
name: 'create Image',
type: ['header', 'footer'],
iconName: 'plus',
actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'create new Image',
content: html`
<dees-form>
<dees-input-text
.label=${'name'}
.key=${'data.name'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'description'}
.key=${'data.description'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'save',
action: async (modalArg) => {
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
const formData = await deesForm.collectFormData();
console.log(`Prepare saving of data:`);
console.log(formData);
await appstate.dataState.dispatchAction(appstate.createImageAction, {
imageName: formData['data.name'] as string,
description: formData['data.description'] as string,
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'edit',
type: ['contextmenu', 'inRow', 'doubleClick'],
iconName: 'penToSquare',
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const environmentsArray: Array<
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
environment: string;
}
> = [];
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
environmentsArray.push({
environment: environmentName,
...dataArg.item.data.environments[environmentName],
});
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit Secret',
content: html`
<dees-form>
<dees-input-text
.key=${'id'}
.disabled=${true}
.label=${'ID'}
.value=${dataArg.item.id}
></dees-input-text>
<dees-input-text
.key=${'data.name'}
.disabled=${false}
.label=${'name'}
.value=${dataArg.item.data.name}
></dees-input-text>
<dees-input-text
.key=${'data.description'}
.disabled=${false}
.label=${'description'}
.value=${dataArg.item.data.description}
></dees-input-text>
<dees-input-text
.key=${'data.key'}
.disabled=${false}
.label=${'key'}
.value=${dataArg.item.data.key}
></dees-input-text>
<dees-table
.key=${'environments'}
.heading1=${'Environments'}
.heading2=${'double-click to edit values'}
.data=${environmentsArray.map((itemArg) => {
return {
environment: itemArg.environment,
value: itemArg.value,
};
})}
.editableFields=${['environment', 'value']}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (actionDataArg) => {
actionDataArg.table.data.splice(
actionDataArg.table.data.indexOf(actionDataArg.item),
1
);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: null,
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Save',
iconName: null,
action: async (modalArg) => {
const data = await modalArg.shadowRoot
.querySelector('dees-form')
.collectFormData();
console.log(data);
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
id: dataArg.item.id,
data: {
name: data['data.name'] as string,
description: data['data.description'] as string,
key: data['data.key'] as string,
environments: {},
tags: dataArg.item.data.tags,
},
};
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of data['environments'] as any[]) {
}
},
},
],
});
},
},
{
name: 'history',
iconName: 'clockRotateLeft',
type: ['contextmenu', 'inRow'],
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const historyArray: Array<{
environment: string;
value: string;
}> = [];
for (const environment of Object.keys(dataArg.item.data.environments)) {
for (const historyItem of dataArg.item.data.environments[environment].history) {
historyArray.push({
environment,
value: historyItem.value,
});
}
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `history for ${dataArg.item.data.key}`,
content: html`
<dees-table
.data=${historyArray}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
) => {
console.log('delete', itemArg);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`,
menuOptions: [
{
name: 'close',
action: async (modalArg) => {
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>
) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Image "${itemArg.item.data.name}"`,
content: html`
<div style="text-align:center">Do you really want to delete the image?</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${itemArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
console.log(`Delete ${itemArg.item.id}`);
await appstate.dataState.dispatchAction(appstate.deleteImageAction, {
imageId: itemArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,130 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-logs')
export class CloudlyViewLogs extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>Logs</cloudly-sectionheading>
<dees-table
.heading1=${'Logs'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,128 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-mails')
export class CloudlyViewMails extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css``
];
public render() {
return html`
<cloudly-sectionheading>Mails</cloudly-sectionheading>
<dees-table
.heading1=${'Mails'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,156 +0,0 @@
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-overview')
export class CloudlyViewOverview extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
dees-statsgrid {
margin-top: 24px;
}
`,
];
public render() {
// Calculate total nodes across all clusters
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
sum + (cluster.data.nodes?.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: 'nodes',
title: 'Total Nodes',
value: totalNodes,
type: 'number' as const,
iconName: 'lucide:Server',
description: 'Connected nodes'
},
{
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>
<dees-statsgrid
.tiles=${statsTiles}
.minTileWidth=${250}
.gap=${16}
></dees-statsgrid>
`;
}
}

View File

@@ -1,130 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-s3')
export class CloudlyViewS3 extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>S3</cloudly-sectionheading>
<dees-table
.heading1=${'S3'}
.heading2=${'decoded in client'}
.data=${this.data.s3}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add configBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add ConfigBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,170 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-secretbundles')
export class CloudlyViewSecretBundles extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subscription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subscription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
<dees-table
.heading1=${'SecretBundles'}
.heading2=${'decoded in client'}
.data=${this.data.secretBundles}
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
return {
name: itemArg.data.name,
secretGroups: (() => {
const secretGroupIds = itemArg.data.includedSecretGroupIds;
let secretGroupNames: string[] = [];
for (const secretGroupId of secretGroupIds) {
const secretGroup = this.data.secretGroups.find(
(secretGroupArg) => secretGroupArg.id === secretGroupId
);
if (secretGroup) {
secretGroupNames.push(secretGroup.data.name);
}
}
return secretGroupNames.join(', ');
})(),
tags: html`<dees-chips
.selectionMode=${'none'}
.selectableChips=${itemArg.data.includedTags}
></dees-chips>`,
};
}}
.dataActions=${[
{
name: 'add SecretBundle',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async (dataActionArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add SecretBundle',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text
.key=${'data.secretGroupIds'}
.label=${'secretGroupIds'}
.value=${''}
></dees-input-text>
<dees-input-text
.key=${'data.includedTags'}
.label=${'includedTags'}
.value=${''}
></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'create', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">
Do you really want to delete the ConfigBundle?
</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${actionDataArg.item.id}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, {
configBundleId: actionDataArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'edit',
iconName: 'penToSquare',
type: ['doubleClick', 'contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit SecretBundle',
content: html`
<dees-form>
<dees-input-text .label=${'purpose'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'save', action: async (modalArg) => {} },
{
name: 'cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,364 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-secretsgroups')
export class CloudlyViewSecretGroups extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
`,
];
public render() {
return html`
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
<dees-table
heading1="SecretGroups"
heading2="decoded in client"
.data=${this.data.secretGroups}
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
return {
name: secretGroup.data.name,
priority: secretGroup.data.priority,
tags: html`<dees-chips
.selectionMode=${'none'}
.selectableChips=${secretGroup.data.tags}
></dees-chips>`,
key: secretGroup.data.key,
history: (() => {
const allHistory = [];
for (const environment in secretGroup.data.environments) {
allHistory.push(...secretGroup.data.environments[environment].history);
}
return allHistory.length;
})(),
};
}}
.dataActions=${[
{
name: 'add SecretGroup',
type: ['header', 'footer'],
iconName: 'plus',
actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'create new SecretGroup',
content: html`
<dees-form>
<dees-input-text
.label=${'name'}
.key=${'data.name'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'description'}
.key=${'data.description'}
.value=${''}
></dees-input-text>
<dees-input-text
.label=${'Secret Key (data.key)'}
.key=${'data.key'}
.value=${''}
></dees-input-text>
<dees-table
.heading1=${'Environments'}
.heading2=${'keys need to be unique'}
key="environments"
.data=${[
{
environment: 'production',
value: '',
},
{
environment: 'staging',
value: '',
},
]}
.dataActions=${[
{
name: 'add environment',
iconName: 'plus',
type: ['footer'],
actionFunc: async (dataArg) => {
dataArg.table.data.push({
environment: 'new environment',
value: '',
});
dataArg.table.requestUpdate('data');
},
},
{
name: 'delete environment',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (dataArg) => {
dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1);
dataArg.table.requestUpdate('data');
},
},
] as plugins.deesCatalog.ITableAction[]}
.editableFields=${['environment', 'value']}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'save',
action: async (modalArg) => {
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
const formData = await deesForm.collectFormData();
console.log(`Prepare saving of data:`);
console.log(formData);
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of formData['environments'] as any[]) {
environments[itemArg.environment] = {
value: itemArg.value,
history: [],
lastUpdated: Date.now(),
};
}
await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, {
id: null,
data: {
name: formData['data.name'] as string,
description: formData['data.description'] as string,
key: formData['data.key'] as string,
environments,
tags: [],
},
});
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'edit',
type: ['contextmenu', 'inRow', 'doubleClick'],
iconName: 'penToSquare',
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const environmentsArray: Array<
plugins.interfaces.data.ISecretGroup['data']['environments'][any] & {
environment: string;
}
> = [];
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
environmentsArray.push({
environment: environmentName,
...dataArg.item.data.environments[environmentName],
});
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit Secret',
content: html`
<dees-form>
<dees-input-text
.key=${'id'}
.disabled=${true}
.label=${'ID'}
.value=${dataArg.item.id}
></dees-input-text>
<dees-input-text
.key=${'data.name'}
.disabled=${false}
.label=${'name'}
.value=${dataArg.item.data.name}
></dees-input-text>
<dees-input-text
.key=${'data.description'}
.disabled=${false}
.label=${'description'}
.value=${dataArg.item.data.description}
></dees-input-text>
<dees-input-text
.key=${'data.key'}
.disabled=${false}
.label=${'key'}
.value=${dataArg.item.data.key}
></dees-input-text>
<dees-table
.key=${'environments'}
.heading1=${'Environments'}
.heading2=${'double-click to edit values'}
.data=${environmentsArray.map((itemArg) => {
return {
environment: itemArg.environment,
value: itemArg.value,
};
})}
.editableFields=${['environment', 'value']}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['inRow'],
actionFunc: async (actionDataArg) => {
actionDataArg.table.data.splice(
actionDataArg.table.data.indexOf(actionDataArg.item),
1
);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
</dees-form>
`,
menuOptions: [
{
name: 'Cancel',
iconName: null,
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Save',
iconName: null,
action: async (modalArg) => {
const data = await modalArg.shadowRoot
.querySelector('dees-form')
.collectFormData();
console.log(data);
const updatedSecretGroup: plugins.interfaces.data.ISecretGroup = {
id: dataArg.item.id,
data: {
name: data['data.name'] as string,
description: data['data.description'] as string,
key: data['data.key'] as string,
environments: {},
tags: dataArg.item.data.tags,
},
};
const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] =
{};
for (const itemArg of data['environments'] as any[]) {
}
},
},
],
});
},
},
{
name: 'history',
iconName: 'clockRotateLeft',
type: ['contextmenu', 'inRow'],
actionFunc: async (
dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
const historyArray: Array<{
environment: string;
value: string;
}> = [];
for (const environment of Object.keys(dataArg.item.data.environments)) {
for (const historyItem of dataArg.item.data.environments[environment].history) {
historyArray.push({
environment,
value: historyItem.value,
});
}
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `history for ${dataArg.item.data.key}`,
content: html`
<dees-table
.data=${historyArray}
.dataActions=${[
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>
) => {
console.log('delete', itemArg);
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`,
menuOptions: [
{
name: 'close',
action: async (modalArg) => {
await modalArg.destroy();
},
},
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (
itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>
) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ${itemArg.item.data.key}`,
content: html`
<div style="text-align:center">Do you really want to delete the secret?</div>
<div
style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;"
>
${itemArg.item.data.key}
</div>
`,
menuOptions: [
{
name: 'cancel',
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'delete',
action: async (modalArg) => {
console.log(`Delete ${itemArg.item.id}`);
await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, {
secretGroupId: itemArg.item.id,
});
await modalArg.destroy();
},
},
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}

View File

@@ -1,478 +0,0 @@
import * as plugins from '../plugins.js';
import * as shared from '../elements/shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
property,
} from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
@customElement('cloudly-view-settings')
export class CloudlyViewSettings extends DeesElement {
@state()
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {};
@state()
private isLoading = false;
@state()
private testResults: {[key: string]: {success: boolean; message: string}} = {};
constructor() {
super();
this.loadSettings();
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.settings-container {
padding: 24px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.provider-icon {
margin-right: 8px;
font-size: 20px;
}
.test-status {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.test-status dees-button {
margin-left: auto;
}
.loading-container {
display: flex;
justify-content: center;
padding: 48px;
}
.actions-container {
display: flex;
justify-content: center;
margin-top: 24px;
}
dees-panel {
margin-bottom: 16px;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-grid.single {
grid-template-columns: 1fr;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
}
`,
];
private async loadSettings() {
this.isLoading = true;
try {
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
plugins.interfaces.requests.settings.IRequest_GetSettings
>(
'/typedrequest',
'getSettings'
);
const response = await trRequest.fire({});
this.settings = response.settings;
} catch (error) {
console.error('Failed to load settings:', error);
plugins.deesCatalog.DeesToast.createAndShow({
message: `Failed to load settings: ${error.message}`,
type: 'error',
});
} finally {
this.isLoading = false;
}
}
private async saveSettings(formData: any) {
console.log('saveSettings called with formData:', formData);
this.isLoading = true;
try {
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
// Process form data
for (const [key, value] of Object.entries(formData)) {
console.log(`Processing ${key}:`, value);
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
// Only update if value changed (not masked)
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
}
}
console.log('Updates to send:', updates);
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
plugins.interfaces.requests.settings.IRequest_UpdateSettings
>(
'/typedrequest',
'updateSettings'
);
const response = await trRequest.fire({ updates });
if (response.success) {
plugins.deesCatalog.DeesToast.createAndShow({
message: 'Settings saved successfully',
type: 'success',
});
await this.loadSettings(); // Reload to get masked values
} else {
throw new Error(response.message);
}
} catch (error) {
console.error('Failed to save settings:', error);
plugins.deesCatalog.DeesToast.createAndShow({
message: `Failed to save settings: ${error.message}`,
type: 'error',
});
} finally {
this.isLoading = false;
}
}
private async testConnection(provider: string) {
this.isLoading = true;
try {
const trRequest = new plugins.deesDomtools.plugins.typedrequest.TypedRequest<
plugins.interfaces.requests.settings.IRequest_TestProviderConnection
>(
'/typedrequest',
'testProviderConnection'
);
const response = await trRequest.fire({ provider: provider as any });
this.testResults = {
...this.testResults,
[provider]: {
success: response.connectionValid,
message: response.message
}
};
// Show toast notification
plugins.deesCatalog.DeesToast.createAndShow({
message: response.message,
type: response.connectionValid ? 'success' : 'error',
});
} catch (error) {
this.testResults = {
...this.testResults,
[provider]: {
success: false,
message: `Test failed: ${error.message}`
}
};
plugins.deesCatalog.DeesToast.createAndShow({
message: `Connection test failed: ${error.message}`,
type: 'error',
});
} finally {
this.isLoading = false;
}
}
private renderProviderStatus(provider: string) {
const result = this.testResults[provider];
if (!result) return '';
return html`
<dees-badge
.type=${result.success ? 'success' : 'error'}
.text=${result.success ? 'Connected' : 'Failed'}
></dees-badge>
`;
}
public render() {
if (this.isLoading && Object.keys(this.settings).length === 0) {
return html`
<div class="loading-container">
<dees-spinner></dees-spinner>
</div>
`;
}
return html`
<cloudly-sectionheading>Settings</cloudly-sectionheading>
<div class="settings-container">
<dees-form @formData=${(e: CustomEvent) => {
console.log('formData event received:', e);
console.log('Event detail:', e.detail);
console.log('Event detail.data:', e.detail.data);
this.saveSettings(e.detail.data);
}}>
<!-- Hetzner Cloud -->
<dees-panel
.title=${'Hetzner Cloud'}
.subtitle=${'Configure Hetzner Cloud API access'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('hetzner')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('hetzner');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'hetznerToken'}
.label=${'API Token'}
.value=${this.settings.hetznerToken || ''}
.isPasswordBool=${true}
.description=${'Your Hetzner Cloud API token for managing infrastructure'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- Cloudflare -->
<dees-panel
.title=${'Cloudflare'}
.subtitle=${'Configure Cloudflare API access'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('cloudflare')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('cloudflare');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'cloudflareToken'}
.label=${'API Token'}
.value=${this.settings.cloudflareToken || ''}
.isPasswordBool=${true}
.description=${'Cloudflare API token with DNS and Zone permissions'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- AWS -->
<dees-panel
.title=${'Amazon Web Services'}
.subtitle=${'Configure AWS credentials'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('aws')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('aws');
}}
></dees-button>
</div>
<div class="form-grid">
<dees-input-text
.key=${'awsAccessKey'}
.label=${'Access Key ID'}
.value=${this.settings.awsAccessKey || ''}
.isPasswordBool=${true}
.description=${'AWS IAM access key identifier'}
.required=${false}
></dees-input-text>
<dees-input-text
.key=${'awsSecretKey'}
.label=${'Secret Access Key'}
.value=${this.settings.awsSecretKey || ''}
.isPasswordBool=${true}
.description=${'AWS IAM secret access key'}
.required=${false}
></dees-input-text>
</div>
<div class="form-grid single">
<dees-input-dropdown
.key=${'awsRegion'}
.label=${'Default Region'}
.selectedOption=${this.settings.awsRegion || 'us-east-1'}
.options=${[
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
]}
.description=${'Default AWS region for resource provisioning'}
></dees-input-dropdown>
</div>
</dees-panel>
<!-- DigitalOcean -->
<dees-panel
.title=${'DigitalOcean'}
.subtitle=${'Configure DigitalOcean API access'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('digitalocean')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('digitalocean');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'digitalOceanToken'}
.label=${'Personal Access Token'}
.value=${this.settings.digitalOceanToken || ''}
.isPasswordBool=${true}
.description=${'DigitalOcean personal access token with read/write scope'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- Azure -->
<dees-panel
.title=${'Microsoft Azure'}
.subtitle=${'Configure Azure service principal'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('azure')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('azure');
}}
></dees-button>
</div>
<div class="form-grid">
<dees-input-text
.key=${'azureClientId'}
.label=${'Application (Client) ID'}
.value=${this.settings.azureClientId || ''}
.isPasswordBool=${true}
.description=${'Azure AD application client ID'}
.required=${false}
></dees-input-text>
<dees-input-text
.key=${'azureClientSecret'}
.label=${'Client Secret'}
.value=${this.settings.azureClientSecret || ''}
.isPasswordBool=${true}
.description=${'Azure AD application client secret'}
.required=${false}
></dees-input-text>
</div>
<div class="form-grid">
<dees-input-text
.key=${'azureTenantId'}
.label=${'Directory (Tenant) ID'}
.value=${this.settings.azureTenantId || ''}
.description=${'Azure AD tenant identifier'}
.required=${false}
></dees-input-text>
<dees-input-text
.key=${'azureSubscriptionId'}
.label=${'Subscription ID'}
.value=${this.settings.azureSubscriptionId || ''}
.description=${'Azure subscription for resource management'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<!-- Google Cloud -->
<dees-panel
.title=${'Google Cloud Platform'}
.subtitle=${'Configure GCP service account'}
.variant=${'outline'}
>
<div class="test-status">
${this.renderProviderStatus('google')}
<dees-button
.text=${'Test Connection'}
.type=${'secondary'}
@click=${(e: Event) => {
e.preventDefault();
e.stopPropagation();
this.testConnection('google');
}}
></dees-button>
</div>
<div class="form-grid single">
<dees-input-textarea
.key=${'googleCloudKeyJson'}
.label=${'Service Account Key (JSON)'}
.value=${this.settings.googleCloudKeyJson || ''}
.isPasswordBool=${true}
.description=${'Complete JSON key file for service account authentication'}
.required=${false}
></dees-input-textarea>
</div>
<div class="form-grid single">
<dees-input-text
.key=${'googleCloudProjectId'}
.label=${'Project ID'}
.value=${this.settings.googleCloudProjectId || ''}
.description=${'Google Cloud project identifier'}
.required=${false}
></dees-input-text>
</div>
</dees-panel>
<div class="actions-container">
<dees-form-submit
.text=${'Save All Settings'}
.disabled=${this.isLoading}
></dees-form-submit>
</div>
</dees-form>
</div>
`;
}
}

View File

@@ -1,4 +1,4 @@
export * from './shared/index.js'; export * from './shared/index.js';
export * from './cloudly-dashboard.js'; export * from './cloudly-dashboard.js';
export * from './cloudly-view-secretgroups.js'; export * from './views/secretgroups/index.js';
export * from './cloudly-view-secretbundles.js'; export * from './views/secretbundles/index.js';

View File

@@ -0,0 +1,52 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-backups')
export class CloudlyViewBackups extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subecription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>Backups</cloudly-sectionheading>
<dees-table
.heading1=${'Backups'}
.heading2=${'decoded in client'}
.data=${this.data.backups}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
.dataActions=${[
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
</dees-form>
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-backups': CloudlyViewBackups; } }

View File

@@ -0,0 +1,111 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-clusters')
export class CloudlyViewClusters extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css``,
];
public render() {
return html`
<cloudly-sectionheading>Clusters</cloudly-sectionheading>
<dees-table
.heading1=${'Clusters'}
.heading2=${'decoded in client'}
.data=${this.data.clusters}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return {
id: itemArg.id,
serverAmount: itemArg.data.servers.length,
};
}}
.dataActions=${[
{
name: 'add cluster',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Cluster',
content: html`
<dees-form>
<dees-input-text .key=${'clusterName'} .label=${'cluster name'} .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: [
{ name: 'create', action: async (modalArg: any) => {
const data = (await modalArg.shadowRoot.querySelector('dees-form').collectFormData()) as any;
await appstate.dataState.dispatchAction(appstate.addClusterAction, data);
await modalArg.destroy();
}},
{ name: 'cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete ConfigBundle ${actionDataArg.item.id}`,
content: html`
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
`,
menuOptions: [
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } },
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-clusters': CloudlyViewClusters;
}
}

View File

@@ -0,0 +1,52 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-dbs')
export class CloudlyViewDbs extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subecription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>DBs</cloudly-sectionheading>
<dees-table
.heading1=${'DBs'}
.heading2=${'decoded in client'}
.data=${this.data.dbs}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
.dataActions=${[
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
</dees-form>
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dbs': CloudlyViewDbs; } }

View File

@@ -0,0 +1,222 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-deployments')
export class CloudlyViewDeployments extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
constructor() {
super();
const subscription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subscription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
.status-running { background: #4caf50; color: white; }
.status-stopped { background: #f44336; color: white; }
.status-paused { background: #ff9800; color: white; }
.status-deploying { background: #2196f3; color: white; }
.health-indicator { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; }
.health-healthy { background: #e8f5e9; color: #2e7d32; }
.health-unhealthy { background: #ffebee; color: #c62828; }
.health-unknown { background: #f5f5f5; color: #666; }
.resource-usage { display: flex; gap: 12px; font-size: 0.9em; color: #888; }
.resource-item { display: flex; align-items: center; gap: 4px; }
`,
];
private getServiceName(serviceId: string): string {
const service = this.data.services?.find(s => s.id === serviceId);
return service?.data?.name || serviceId;
}
private getNodeName(nodeId: string): string {
return nodeId.substring(0, 8);
}
private getStatusBadgeHtml(status: string): any {
const className = `status-badge status-${status}`;
return html`<span class="${className}">${status}</span>`;
}
private getHealthIndicatorHtml(health?: string): any {
if (!health) health = 'unknown';
const className = `health-indicator health-${health}`;
const icon = health === 'healthy' ? '✓' : health === 'unhealthy' ? '✗' : '?';
return html`<span class="${className}">${icon} ${health}</span>`;
}
private getResourceUsageHtml(deployment: plugins.interfaces.data.IDeployment): any {
if (!deployment.resourceUsage) {
return html`<span style="color: #aaa;">N/A</span>`;
}
const { cpuUsagePercent, memoryUsedMB } = deployment.resourceUsage;
return html`
<div class="resource-usage">
<div class="resource-item">
<lucide-icon name="Cpu" size="14"></lucide-icon>
${cpuUsagePercent?.toFixed(1) || 0}%
</div>
<div class="resource-item">
<lucide-icon name="MemoryStick" size="14"></lucide-icon>
${memoryUsedMB || 0} MB
</div>
</div>
`;
}
public render() {
return html`
<cloudly-sectionheading>Deployments</cloudly-sectionheading>
<dees-table
.heading1=${'Deployments'}
.heading2=${'Service deployments running on cluster nodes'}
.data=${this.data.deployments || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IDeployment) => {
return {
Service: this.getServiceName(itemArg.serviceId),
Node: this.getNodeName(itemArg.nodeId),
Status: this.getStatusBadgeHtml(itemArg.status),
Health: this.getHealthIndicatorHtml(itemArg.healthStatus),
'Container ID': itemArg.containerId ? html`<span style="font-family: monospace; font-size: 0.9em;">${itemArg.containerId.substring(0, 12)}</span>` : html`<span style="color: #aaa;">N/A</span>`,
Version: itemArg.version || 'latest',
'Resource Usage': this.getResourceUsageHtml(itemArg),
'Last Updated': itemArg.deployedAt ? new Date(itemArg.deployedAt).toLocaleString() : 'Never',
};
}}
.dataActions=${[
{
name: 'Deploy Service',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async () => {
const availableServices = this.data.services || [];
if (availableServices.length === 0) {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'No Services Available',
content: html`<div style="text-align: center; padding: 24px;"><lucide-icon name="AlertCircle" size="48" style="color: #ff9800; margin-bottom: 16px;"></lucide-icon><div>Please create a service first before creating deployments.</div></div>`,
menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ],
});
return;
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Deploy Service',
content: html`
<dees-form>
<dees-input-dropdown .key=${'serviceId'} .label=${'Service'} .options=${availableServices.map(s => ({ key: s.id, value: s.data.name }))} .required=${true}></dees-input-dropdown>
<dees-input-text .key=${'nodeId'} .label=${'Target Node ID'} .required=${true} .description=${'Enter the cluster node ID where this service should be deployed'}></dees-input-text>
<dees-input-text .key=${'version'} .label=${'Version'} .value=${'latest'} .required=${true}></dees-input-text>
<dees-input-dropdown .key=${'status'} .label=${'Initial Status'} .options=${['deploying', 'running']} .value=${'deploying'} .required=${true}></dees-input-dropdown>
</dees-form>
`,
menuOptions: [
{ name: 'Deploy', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.createDeploymentAction, {
deploymentData: {
serviceId: formData.serviceId,
nodeId: formData.nodeId,
status: formData.status,
version: formData.version,
deployedAt: Date.now(),
usedImageId: 'placeholder',
deploymentLog: [],
},
});
await modalArg.destroy();
}},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
},
},
{
name: 'Restart',
iconName: 'refresh-cw',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Restart Deployment`,
content: html`
<div style="text-align:center">Are you sure you want to restart this deployment?</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div>
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div>
</div>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'Restart', action: async (modalArg: any) => { console.log('Restart deployment:', deployment); await modalArg.destroy(); } },
],
});
},
},
{
name: 'Stop',
iconName: 'square',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
await appstate.dataState.dispatchAction(appstate.updateDeploymentAction, {
deploymentId: deployment.id,
deploymentData: { ...deployment, status: 'stopped' },
});
},
},
{
name: 'Delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
const deployment = actionDataArg.item as plugins.interfaces.data.IDeployment;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Deployment`,
content: html`
<div style="text-align:center">Are you sure you want to delete this deployment?</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold;">${this.getServiceName(deployment.serviceId)}</div>
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">Node: ${this.getNodeName(deployment.nodeId)}</div>
<div style="color: #f44336; margin-top: 8px;">This action cannot be undone.</div>
</div>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDeploymentAction, { deploymentId: deployment.id, }); await modalArg.destroy(); } },
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-deployments': CloudlyViewDeployments;
}
}

View File

@@ -0,0 +1,155 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-dns')
export class CloudlyViewDns extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], dnsEntries: [], domains: [] } as any;
constructor() {
super();
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subscription);
}
async connectedCallback() {
super.connectedCallback();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.dns-type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
.type-A { background: #4CAF50; }
.type-AAAA { background: #45a049; }
.type-CNAME { background: #2196F3; }
.type-MX { background: #FF9800; }
.type-TXT { background: #9C27B0; }
.type-NS { background: #795548; }
.type-SOA { background: #607D8B; }
.type-SRV { background: #E91E63; }
.type-CAA { background: #00BCD4; }
.type-PTR { background: #673AB7; }
.status-active { color: #4CAF50; }
.status-inactive { color: #f44336; }
`,
];
private getRecordTypeBadge(type: string) { return html`<span class="dns-type-badge type-${type}">${type}</span>`; }
private getStatusBadge(active: boolean) { return html`<span class="${active ? 'status-active' : 'status-inactive'}">${active ? '✓ Active' : '✗ Inactive'}</span>`; }
public render() {
return html`
<cloudly-sectionheading>DNS Management</cloudly-sectionheading>
<dees-table
.heading1=${'DNS Entries'}
.heading2=${'Manage DNS records for your domains'}
.data=${this.data.dnsEntries || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IDnsEntry) => {
return {
Type: this.getRecordTypeBadge(itemArg.data.type),
Name: itemArg.data.name === '@' ? '<root>' : itemArg.data.name,
Value: itemArg.data.value,
TTL: `${itemArg.data.ttl}s`,
Priority: itemArg.data.priority || '-',
Zone: itemArg.data.zone,
Status: this.getStatusBadge(itemArg.data.active),
Description: itemArg.data.description || '-',
};
}}
.dataActions=${[
{ name: 'Add DNS Entry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add DNS Entry',
content: html`
<dees-form>
<dees-input-dropdown .key=${'type'} .label=${'Record Type'} .options=${[
{key: 'A', option: 'A - IPv4 Address'}, {key: 'AAAA', option: 'AAAA - IPv6 Address'}, {key: 'CNAME', option: 'CNAME - Canonical Name'}, {key: 'MX', option: 'MX - Mail Exchange'}, {key: 'TXT', option: 'TXT - Text Record'}, {key: 'NS', option: 'NS - Name Server'}, {key: 'SOA', option: 'SOA - Start of Authority'}, {key: 'SRV', option: 'SRV - Service'}, {key: 'CAA', option: 'CAA - Certification Authority'}, {key: 'PTR', option: 'PTR - Pointer'}, ]} .value=${'A'} .required=${true}></dees-input-dropdown>
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .required=${true}></dees-input-dropdown>
<dees-input-text .key=${'name'} .label=${'Name'} .placeholder=${'@ for root, www, mail, etc.'} .value=${'@'} .required=${true}></dees-input-text>
<dees-input-text .key=${'value'} .label=${'Value'} .placeholder=${'IP address, domain, or text value'} .required=${true}></dees-input-text>
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${'3600'} .type=${'number'} .required=${true}></dees-input-text>
<dees-input-text .key=${'priority'} .label=${'Priority (MX/SRV only)'} .type=${'number'} .placeholder=${'10'}></dees-input-text>
<dees-input-text .key=${'weight'} .label=${'Weight (SRV only)'} .type=${'number'} .placeholder=${'0'}></dees-input-text>
<dees-input-text .key=${'port'} .label=${'Port (SRV only)'} .type=${'number'} .placeholder=${'443'}></dees-input-text>
<dees-input-checkbox .key=${'active'} .label=${'Active'} .value=${true}></dees-input-checkbox>
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'What is this record for?'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Create DNS Entry', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
// Guard: only allow on activated domains
const domain = (this.data.domains || []).find((d: any) => d.id === formData.domainId);
if (!domain || (domain.data as any).activationState !== 'activated') {
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Selected domain is not activated. Activate it first.', type: 'error' });
return;
}
await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { type: formData.type, domainId: formData.domainId, zone: '', name: formData.name || '@', value: formData.value, ttl: parseInt(formData.ttl) || 3600, priority: formData.priority ? parseInt(formData.priority) : undefined, weight: formData.weight ? parseInt(formData.weight) : undefined, port: formData.port ? parseInt(formData.port) : undefined, active: formData.active, description: formData.description || undefined, }, });
await modalArg.destroy();
} },
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
} },
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry;
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Edit DNS Entry`,
content: html`
<dees-form>
<dees-input-dropdown .key=${'type'} .label=${'Record Type'} .options=${[
{key: 'A', option: 'A - IPv4 Address'}, {key: 'AAAA', option: 'AAAA - IPv6 Address'}, {key: 'CNAME', option: 'CNAME - Canonical Name'}, {key: 'MX', option: 'MX - Mail Exchange'}, {key: 'TXT', option: 'TXT - Text Record'}, {key: 'NS', option: 'NS - Name Server'}, {key: 'SOA', option: 'SOA - Start of Authority'}, {key: 'SRV', option: 'SRV - Service'}, {key: 'CAA', option: 'CAA - Certification Authority'}, {key: 'PTR', option: 'PTR - Pointer'}, ]} .value=${dnsEntry.data.type} .required=${true}></dees-input-dropdown>
<dees-input-dropdown .key=${'domainId'} .label=${'Domain'} .options=${this.data.domains?.map(domain => ({ key: domain.id, option: domain.data.name })) || []} .value=${dnsEntry.data.domainId || ''} .required=${true}></dees-input-dropdown>
<dees-input-text .key=${'name'} .label=${'Name'} .value=${dnsEntry.data.name} .required=${true}></dees-input-text>
<dees-input-text .key=${'value'} .label=${'Value'} .value=${dnsEntry.data.value} .required=${true}></dees-input-text>
<dees-input-text .key=${'ttl'} .label=${'TTL (seconds)'} .value=${dnsEntry.data.ttl} .type=${'number'} .required=${true}></dees-input-text>
<dees-input-text .key=${'priority'} .label=${'Priority (MX/SRV only)'} .value=${dnsEntry.data.priority || ''} .type=${'number'}></dees-input-text>
<dees-input-text .key=${'weight'} .label=${'Weight (SRV only)'} .value=${dnsEntry.data.weight || ''} .type=${'number'}></dees-input-text>
<dees-input-text .key=${'port'} .label=${'Port (SRV only)'} .value=${dnsEntry.data.port || ''} .type=${'number'}></dees-input-text>
<dees-input-checkbox .key=${'active'} .label=${'Active'} .value=${dnsEntry.data.active}></dees-input-checkbox>
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${dnsEntry.data.description || ''}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Update DNS Entry', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
if (formData.domainId) {
const domain = (this.data.domains || []).find((d: any) => d.id === formData.domainId);
if (!domain || (domain.data as any).activationState !== 'activated') {
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Selected domain is not activated. Activate it first.', type: 'error' });
return;
}
}
await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, { dnsEntryId: dnsEntry.id, dnsEntryData: { ...dnsEntry.data, type: formData.type, domainId: formData.domainId, zone: '', name: formData.name || '@', value: formData.value, ttl: parseInt(formData.ttl) || 3600, priority: formData.priority ? parseInt(formData.priority) : undefined, weight: formData.weight ? parseInt(formData.weight) : undefined, port: formData.port ? parseInt(formData.port) : undefined, active: formData.active, description: formData.description || undefined, }, });
await modalArg.destroy();
} },
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
} },
{ name: 'Duplicate', iconName: 'copy', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; await appstate.dataState.dispatchAction(appstate.createDnsEntryAction, { dnsEntryData: { ...dnsEntry.data, description: `Copy of ${dnsEntry.data.description || dnsEntry.data.name}`, }, }); } },
{ name: 'Toggle Active', iconName: 'power', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; await appstate.dataState.dispatchAction(appstate.updateDnsEntryAction, { dnsEntryId: dnsEntry.id, dnsEntryData: { ...dnsEntry.data, active: !dnsEntry.data.active, }, }); } },
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const dnsEntry = actionDataArg.item as plugins.interfaces.data.IDnsEntry; plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete DNS Entry`, content: html`<div style="text-align:center">Are you sure you want to delete this DNS entry?</div><div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"><div style="color: #fff; font-weight: bold;">${dnsEntry.data.type} - ${dnsEntry.data.name}.${dnsEntry.data.zone}</div><div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${dnsEntry.data.value}</div>${dnsEntry.data.description ? html`<div style=\"color: #888; font-size: 0.85em; margin-top: 8px;\">${dnsEntry.data.description}</div>` : ''}</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDnsEntryAction, { dnsEntryId: dnsEntry.id, }); await modalArg.destroy(); } }, ], }); } },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-dns': CloudlyViewDns; } }

View File

@@ -0,0 +1,185 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-domains')
export class CloudlyViewDomains extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], domains: [], dnsEntries: [] } as any;
constructor() {
super();
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subscription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
.status-active { background: #4CAF50; }
.status-pending { background: #FF9800; }
.status-expired { background: #f44336; }
.status-suspended { background: #9E9E9E; }
.status-transferred { background: #607D8B; }
.verification-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
.verification-verified { background: #4CAF50; color: white; }
.verification-pending { background: #FF9800; color: white; }
.verification-failed { background: #f44336; color: white; }
.verification-not_required { background: #E0E0E0; color: #333; }
.ssl-badge { display: inline-block; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; }
.ssl-active { color: #4CAF50; }
.ssl-pending { color: #FF9800; }
.ssl-expired { color: #f44336; }
.ssl-none { color: #9E9E9E; }
.nameserver-list { font-size: 0.85em; color: #666; }
.activation-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
.activation-available { background: #2b2b2b; color: #bbb; border: 1px solid #444; }
.activation-activated { background: #4CAF50; color: #fff; }
.activation-ignored { background: #9E9E9E; color: #fff; }
.expiry-warning { color: #FF9800; font-weight: 500; }
.expiry-critical { color: #f44336; font-weight: bold; }
`,
];
private getStatusBadge(status: string) { return html`<span class="status-badge status-${status}">${status.toUpperCase()}</span>`; }
private getVerificationBadge(status: string) { const displayText = status === 'not_required' ? 'Not Required' : status.replace('_', ' ').toUpperCase(); return html`<span class="verification-badge verification-${status}">${displayText}</span>`; }
private getSslBadge(sslStatus?: string) { if (!sslStatus) return html`<span class="ssl-badge ssl-none">—</span>`; const icon = sslStatus === 'active' ? '🔒' : sslStatus === 'expired' ? '⚠️' : '🔓'; return html`<span class="ssl-badge ssl-${sslStatus}">${icon} ${sslStatus.toUpperCase()}</span>`; }
private getActivationBadge(state?: 'available'|'activated'|'ignored') { const s = state || 'available'; return html`<span class="activation-badge activation-${s}">${s.toUpperCase()}</span>`; }
private formatDate(timestamp?: number) { if (!timestamp) return '—'; const date = new Date(timestamp); return date.toLocaleDateString(); }
private getDaysUntilExpiry(expiresAt?: number) { if (!expiresAt) return null; const days = Math.floor((expiresAt - Date.now()) / (1000 * 60 * 60 * 24)); return days; }
private getExpiryDisplay(expiresAt?: number) { const days = this.getDaysUntilExpiry(expiresAt); if (days === null) return '—'; if (days < 0) { return html`<span class="expiry-critical">Expired ${Math.abs(days)} days ago</span>`; } else if (days <= 30) { return html`<span class="expiry-warning">Expires in ${days} days</span>`; } else { return `${days} days`; } }
public render() {
return html`
<cloudly-sectionheading>Domain Management</cloudly-sectionheading>
<dees-table
.heading1=${'Domains'}
.heading2=${'Manage your domains and DNS zones'}
.data=${this.data.domains || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IDomain) => {
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === itemArg.data.name).length || 0;
return {
Domain: html`<div><div style="font-weight: 500;">${itemArg.data.name}</div>${itemArg.data.description ? html`<div style="font-size: 0.85em; color: #666; margin-top: 2px;">${itemArg.data.description}</div>` : ''}</div>`,
Status: this.getStatusBadge(itemArg.data.status),
Verification: this.getVerificationBadge(itemArg.data.verificationStatus),
SSL: this.getSslBadge(itemArg.data.sslStatus),
Activation: this.getActivationBadge((itemArg.data as any).activationState),
'DNS Records': dnsCount,
Registrar: itemArg.data.registrar?.name || '—',
Expires: this.getExpiryDisplay(itemArg.data.expiresAt),
'Auto-Renew': itemArg.data.autoRenew ? '✓' : '✗',
Nameservers: html`<div class="nameserver-list">${itemArg.data.nameservers?.join(', ') || '—'}</div>`,
};
}}
.dataActions=${[
{ name: 'Sync from Cloudflare', iconName: 'cloud', type: ['header'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName: 'cloudflare-domain-sync' } as any); await appstate.dataState.dispatchAction(appstate.getAllDataAction, null); plugins.deesCatalog.DeesToast.createAndShow({ message: 'Triggered Cloudflare sync', type: 'success' }); } },
{ name: 'Add Domain', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Domain',
content: html`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Domain Name'} .placeholder=${'example.com'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .placeholder=${'Main company domain'}></dees-input-text>
<dees-input-dropdown .key=${'status'} .label=${'Status'} .options=${[{key: 'active', option: 'Active'}, {key: 'pending', option: 'Pending'}, {key: 'expired', option: 'Expired'}, {key: 'suspended', option: 'Suspended'}]} .value=${'pending'} .required=${true}></dees-input-dropdown>
<dees-input-text .key=${'nameservers'} .label=${'Nameservers (comma separated)'} .placeholder=${'ns1.example.com, ns2.example.com'}></dees-input-text>
<dees-input-text .key=${'registrarName'} .label=${'Registrar Name'} .placeholder=${'GoDaddy, Namecheap, etc.'}></dees-input-text>
<dees-input-text .key=${'registrarUrl'} .label=${'Registrar URL'} .placeholder=${'https://registrar.com'}></dees-input-text>
<dees-input-text .key=${'expiresAt'} .label=${'Expiration Date'} .type=${'date'}></dees-input-text>
<dees-input-checkbox .key=${'autoRenew'} .label=${'Auto-Renew Enabled'} .value=${true}></dees-input-checkbox>
<dees-input-checkbox .key=${'dnssecEnabled'} .label=${'DNSSEC Enabled'} .value=${false}></dees-input-checkbox>
<dees-input-checkbox .key=${'isPrimary'} .label=${'Primary Domain'} .value=${false}></dees-input-checkbox>
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .placeholder=${'production, critical'}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Create Domain', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
const nameservers = formData.nameservers ? formData.nameservers.split(',').map((ns: string) => ns.trim()).filter((ns: string) => ns) : [];
const tags = formData.tags ? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag) : [];
await appstate.dataState.dispatchAction(appstate.createDomainAction, { domainData: { name: formData.name, description: formData.description || undefined, status: formData.status, verificationStatus: 'pending', nameservers, registrar: formData.registrarName ? { name: formData.registrarName, url: formData.registrarUrl || undefined, } : undefined, expiresAt: formData.expiresAt ? new Date(formData.expiresAt).getTime() : undefined, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, isPrimary: formData.isPrimary, tags: tags.length > 0 ? tags : undefined, }, });
await modalArg.destroy();
}},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
} },
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Edit Domain: ${domain.data.name}`,
content: html`
<dees-form>
<dees-input-text .key=${'name'} .label=${'Domain Name'} .value=${domain.data.name} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .value=${domain.data.description || ''}></dees-input-text>
<dees-input-dropdown .key=${'status'} .label=${'Status'} .options=${[{key: 'active', option: 'Active'}, {key: 'pending', option: 'Pending'}, {key: 'expired', option: 'Expired'}, {key: 'suspended', option: 'Suspended'}, {key: 'transferred', option: 'Transferred'}]} .value=${domain.data.status} .required=${true}></dees-input-dropdown>
<dees-input-checkbox .key=${'autoRenew'} .label=${'Auto-Renew Enabled'} .value=${domain.data.autoRenew}></dees-input-checkbox>
<dees-input-checkbox .key=${'dnssecEnabled'} .label=${'DNSSEC Enabled'} .value=${domain.data.dnssecEnabled}></dees-input-checkbox>
<dees-input-text .key=${'tags'} .label=${'Tags (comma separated)'} .value=${(domain.data.tags || []).join(', ')}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Save Changes', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
const tags = formData.tags ? formData.tags.split(',').map((tag: string) => tag.trim()).filter((tag: string) => tag) : [];
await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, updates: { name: formData.name, description: formData.description || undefined, status: formData.status, autoRenew: formData.autoRenew, dnssecEnabled: formData.dnssecEnabled, tags }, });
await modalArg.destroy();
}},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
} },
{ name: 'Verify Ownership', iconName: 'check-circle', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Verify Domain: ${domain.data.name}`,
content: html`
<div style="text-align:center; padding: 20px;">
<p>Choose a verification method for <strong>${domain.data.name}</strong></p>
<dees-form>
<dees-input-dropdown .key=${'method'} .label=${'Verification Method'} .options=${[{key: 'dns', option: 'DNS TXT Record'}, {key: 'http', option: 'HTTP File Upload'}, {key: 'email', option: 'Email Verification'}, {key: 'manual', option: 'Manual Verification'}]} .value=${'dns'} .required=${true}></dees-input-dropdown>
</dees-form>
${domain.data.verificationToken ? html`<div style="margin-top: 20px; padding: 15px; background: #333; border-radius: 8px;"><div style="color: #aaa; font-size: 0.9em;">Verification Token:</div><code style="color: #4CAF50; word-break: break-all;">${domain.data.verificationToken}</code></div>` : ''}
</div>
`,
menuOptions: [
{ name: 'Start Verification', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.verifyDomainAction, { domainId: domain.id, verificationMethod: formData.method, }); await modalArg.destroy(); } },
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
} },
{ name: 'View DNS Records', iconName: 'list', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; console.log('View DNS records for domain:', domain.data.name); } },
{ name: 'Delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
const domain = actionDataArg.item as plugins.interfaces.data.IDomain;
const dnsCount = this.data.dnsEntries?.filter(dns => dns.data.zone === domain.data.name).length || 0;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Domain`,
content: html`
<div style="text-align:center">Are you sure you want to delete this domain?</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold; font-size: 1.1em;">${domain.data.name}</div>
${domain.data.description ? html`<div style="color: #aaa; margin-top: 4px;">${domain.data.description}</div>` : ''}
${dnsCount > 0 ? html`<div style="color: #f44336; margin-top: 12px; padding: 8px; background: #1a1a1a; border-radius: 4px;">⚠️ This domain has ${dnsCount} DNS record${dnsCount > 1 ? 's' : ''} that will also be deleted</div>` : ''}
</div>
`,
menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteDomainAction, { domainId: domain.id, }); await modalArg.destroy(); } }, ],
});
} },
{ name: 'Activate', iconName: 'check', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'activated' } as any }); } },
{ name: 'Deactivate', iconName: 'slash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'available' } as any }); } },
{ name: 'Ignore', iconName: 'ban', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => { const domain = actionDataArg.item as plugins.interfaces.data.IDomain; await appstate.dataState.dispatchAction(appstate.updateDomainAction, { domainId: domain.id, domainData: { ...(domain.data as any), activationState: 'ignored' } as any }); } },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global {
interface HTMLElementTagNameMap { 'cloudly-view-domains': CloudlyViewDomains; }
}

View File

@@ -0,0 +1,118 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-externalregistries')
export class CloudlyViewExternalRegistries extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [], externalRegistries: [] } as any;
constructor() {
super();
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subscription);
}
async connectedCallback() {
super.connectedCallback();
await appstate.dataState.dispatchAction(appstate.getAllDataAction, {});
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
.status-active { background: #4CAF50; }
.status-inactive { background: #9E9E9E; }
.status-error { background: #f44336; }
.status-unverified { background: #FF9800; }
.type-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; color: white; }
.type-docker { background: #2196F3; }
.type-npm { background: #CB3837; }
.default-badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; background: #673AB7; color: white; margin-left: 8px; }
`,
];
public render() {
return html`
<cloudly-sectionheading>External Registries</cloudly-sectionheading>
<dees-table
.heading1=${'External Registries'}
.heading2=${'Configure external Docker and NPM registries'}
.data=${this.data.externalRegistries || []}
.displayFunction=${(registry: plugins.interfaces.data.IExternalRegistry) => {
return {
Name: html`${registry.data.name}${registry.data.isDefault ? html`<span class="default-badge">DEFAULT</span>` : ''}`,
Type: html`<span class="type-badge type-${registry.data.type}">${registry.data.type.toUpperCase()}</span>`,
URL: registry.data.url,
Auth: registry.data.authType === 'none' ? 'Public' : (registry.data.username || 'Token Auth'),
Namespace: registry.data.namespace || '-',
Status: html`<span class="status-badge status-${registry.data.status || 'unverified'}">${(registry.data.status || 'unverified').toUpperCase()}</span>`,
'Last Verified': registry.data.lastVerified ? new Date(registry.data.lastVerified).toLocaleString() : 'Never',
};
}}
.dataActions=${[
{ name: 'Add Registry', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add External Registry', content: html`
<dees-form>
<dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${'docker'} .required=${true}></dees-input-dropdown>
<dees-input-text .key=${'name'} .label=${'Registry Name'} .placeholder=${'My Docker Hub'} .required=${true}></dees-input-text>
<dees-input-text .key=${'url'} .label=${'Registry URL'} .placeholder=${'https://index.docker.io/v2/ or registry.gitlab.com'} .required=${true}></dees-input-text>
<dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .placeholder=${'username or leave empty for token auth'}></dees-input-text>
<dees-input-text .key=${'password'} .label=${'Password / Token (NPM _authToken, Docker access token, etc.)'} .placeholder=${'Token or password'} .isPasswordBool=${true}></dees-input-text>
<dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .placeholder=${'myorg'}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .placeholder=${'Production Docker registry'}></dees-input-text>
<dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${'none'}></dees-input-dropdown>
<dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${false}></dees-input-checkbox>
<dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${false}></dees-input-checkbox>
</dees-form>
`, menuOptions: [ { name: 'Create Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); await appstate.dataState.dispatchAction(appstate.createExternalRegistryAction, { registryData: { type: formData.type, name: formData.name, url: formData.url, username: formData.username, password: formData.password, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
} },
{ name: 'Edit', iconName: 'edit', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Edit Registry: ${registry.data.name}`, content: html`
<dees-form>
<dees-input-dropdown .key=${'type'} .label=${'Registry Type'} .options=${[{key: 'docker', option: 'Docker'}, {key: 'npm', option: 'NPM'}]} .value=${registry.data.type} .required=${true}></dees-input-dropdown>
<dees-input-text .key=${'name'} .label=${'Registry Name'} .value=${registry.data.name} .required=${true}></dees-input-text>
<dees-input-text .key=${'url'} .label=${'Registry URL'} .value=${registry.data.url} .required=${true}></dees-input-text>
<dees-input-text .key=${'username'} .label=${'Username (only needed for basic auth)'} .value=${registry.data.username || ''} .placeholder=${'Leave empty for token auth'}></dees-input-text>
<dees-input-text .key=${'password'} .label=${'Password / Token (leave empty to keep current)'} .placeholder=${'New token or password'} .isPasswordBool=${true}></dees-input-text>
<dees-input-text .key=${'namespace'} .label=${'Namespace/Organization (optional)'} .value=${registry.data.namespace || ''}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description (optional)'} .value=${registry.data.description || ''}></dees-input-text>
<dees-input-dropdown .key=${'authType'} .label=${'Authentication Type'} .options=${[{key: 'none', option: 'No Authentication (Public Registry)'}, {key: 'basic', option: 'Basic Auth (Username + Password)'}, {key: 'token', option: 'Token Only (NPM, GitHub, GitLab tokens)'}, {key: 'oauth2', option: 'OAuth2 (Advanced)'}]} .value=${registry.data.authType || 'none'}></dees-input-dropdown>
<dees-input-checkbox .key=${'isDefault'} .label=${'Set as default registry for this type'} .value=${registry.data.isDefault || false}></dees-input-checkbox>
<dees-input-checkbox .key=${'insecure'} .label=${'Allow insecure connections (HTTP/self-signed certs)'} .value=${registry.data.insecure || false}></dees-input-checkbox>
</dees-form>
`, menuOptions: [ { name: 'Update Registry', action: async (modalArg: any) => { const form = modalArg.shadowRoot.querySelector('dees-form') as any; const formData = await form.gatherData(); const updateData: any = { type: formData.type, name: formData.name, url: formData.url, username: formData.username, namespace: formData.namespace || undefined, description: formData.description || undefined, authType: formData.authType, isDefault: formData.isDefault, insecure: formData.insecure, }; if (formData.password) { updateData.password = formData.password; } await appstate.dataState.dispatchAction(appstate.updateExternalRegistryAction, { registryId: registry.id, updates: updateData, }); await modalArg.destroy(); } }, { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
} },
{ name: 'Test Connection', iconName: 'check-circle', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => {
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
const loadingModal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Testing Registry Connection', content: html`<div style="text-align: center; padding: 20px;"><dees-spinner></dees-spinner><p style="margin-top: 20px;">Testing connection to ${registry.data.name}...</p></div>`, menuOptions: [] });
await appstate.dataState.dispatchAction(appstate.verifyExternalRegistryAction, { registryId: registry.id, });
await loadingModal.destroy();
const updatedRegistry = this.data.externalRegistries?.find(r => r.id === registry.id);
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Connection Test Result', content: html`<div style="text-align: center; padding: 20px;">${updatedRegistry?.data.status === 'active' ? html`<div style="color: #4CAF50; font-size: 48px;">✓</div><p style="margin-top: 20px; color: #4CAF50;">Connection successful!</p>` : html`<div style="color: #f44336; font-size: 48px;">✗</div><p style="margin-top: 20px; color: #f44336;">Connection failed!</p>${updatedRegistry?.data.lastError ? html`<p style="margin-top: 10px; font-size: 0.9em; color: #999;">Error: ${updatedRegistry.data.lastError}</p>` : ''}`}</div>`, menuOptions: [ { name: 'OK', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
} },
{ name: 'Delete', iconName: 'trash', type: ['contextmenu'], actionFunc: async (actionDataArg: any) => {
const registry = actionDataArg.item as plugins.interfaces.data.IExternalRegistry;
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete Registry: ${registry.data.name}`, content: html`<div style="text-align:center"><p>Do you really want to delete this external registry?</p><p style="color: #999; font-size: 0.9em; margin-top: 10px;">This will remove all stored credentials and configuration.</p></div><div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${registry.data.name} (${registry.data.url})</div>`, menuOptions: [ { name: 'Cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteExternalRegistryAction, { registryId: registry.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-externalregistries': CloudlyViewExternalRegistries; } }

View File

@@ -0,0 +1,142 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-images')
export class CloudlyViewImages extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
constructor() {
super();
appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css``,
];
public render() {
return html`
<cloudly-sectionheading>Images</cloudly-sectionheading>
<dees-table
heading1="Images"
heading2="an image is needed for running a service"
.data=${this.data.images}
.displayFunction=${(image: plugins.interfaces.data.IImage) => {
return { id: image.id, name: image.data.name, description: image.data.description, versions: image.data.versions.length };
}}
.dataActions=${[
{
name: 'create Image',
type: ['header', 'footer'],
iconName: 'plus',
actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: 'create new Image',
content: html`
<dees-form>
<dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text>
<dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'save', action: async (modalArg: any) => {
const deesForm = modalArg.shadowRoot.querySelector('dees-form');
const formData = await deesForm.collectFormData();
await appstate.dataState.dispatchAction(appstate.createImageAction, { imageName: formData['data.name'] as string, description: formData['data.description'] as string });
await modalArg.destroy();
} },
],
});
},
},
{
name: 'edit',
type: ['contextmenu', 'inRow', 'doubleClick'],
iconName: 'penToSquare',
actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
for (const environmentName of Object.keys(dataArg.item.data.environments)) {
environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName] });
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Edit Secret',
content: html`
<dees-form>
<dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text>
<dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text>
<dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text>
<dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text>
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'}
.data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value }))}
.editableFields=${['environment', 'value']}
.dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
</dees-table>
</dees-form>
`,
menuOptions: [
{ name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } },
],
});
},
},
{
name: 'history',
iconName: 'clockRotateLeft',
type: ['contextmenu', 'inRow'],
actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
const historyArray: Array<{ environment: string; value: string; }> = [];
for (const environment of Object.keys(dataArg.item.data.environments)) {
for (const historyItem of dataArg.item.data.environments[environment].history) {
historyArray.push({ environment, value: historyItem.value });
}
}
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `history for ${dataArg.item.data.key}`,
content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`,
menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ],
});
},
},
{
name: 'delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.IImage>) => {
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Image "${itemArg.item.data.name}"`,
content: html`
<div style="text-align:center">Do you really want to delete the image?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${itemArg.item.id}</div>
`,
menuOptions: [
{ name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } },
{ name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteImageAction, { imageId: itemArg.item.id, }); await modalArg.destroy(); } },
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-images': CloudlyViewImages;
}
}

View File

@@ -0,0 +1,61 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-logs')
export class CloudlyViewLogs extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subecription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>Logs</cloudly-sectionheading>
<dees-table
.heading1=${'Logs'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return { id: itemArg.id, serverAmount: itemArg.data.servers.length };
}}
.dataActions=${[
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
</dees-form>
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-logs': CloudlyViewLogs; } }

View File

@@ -0,0 +1,61 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-mails')
export class CloudlyViewMails extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subecription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>Mails</cloudly-sectionheading>
<dees-table
.heading1=${'Mails'}
.heading2=${'decoded in client'}
.data=${this.data.deployments}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => {
return { id: itemArg.id, serverAmount: itemArg.data.servers.length };
}}
.dataActions=${[
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
</dees-form>
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
<div style=\"text-align:center\">Do you really want to delete the ConfigBundle?</div>
<div style=\"font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;\">${actionDataArg.item.id}</div>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-mails': CloudlyViewMails; } }

View File

@@ -0,0 +1,71 @@
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-overview')
export class CloudlyViewOverview extends DeesElement {
@state()
private data: appstate.IDataState = {
secretGroups: [],
secretBundles: [],
};
constructor() {
super();
const subecription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subecription);
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
dees-statsgrid { margin-top: 24px; }
`,
];
public render() {
const totalNodes = this.data.clusters?.reduce((sum, cluster) =>
sum + (cluster.data.nodes?.length || 0), 0) || 0;
const statsTiles = [
{ id: 'clusters', title: 'Total Clusters', value: this.data.clusters?.length || 0, type: 'number' as const, iconName: 'lucide:Network', description: 'Active clusters' },
{ id: 'nodes', title: 'Total Nodes', value: totalNodes, type: 'number' as const, iconName: 'lucide:Server', description: 'Connected nodes' },
{ 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 Entries', value: this.data.dnsEntries?.length || 0, type: 'number' as const, iconName: 'lucide:Globe', description: 'Managed DNS records' },
{ 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>
<dees-statsgrid .tiles=${statsTiles} .minTileWidth=${250} .gap=${16}></dees-statsgrid>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-overview': CloudlyViewOverview;
}
}

View File

@@ -0,0 +1,52 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-s3')
export class CloudlyViewS3 extends DeesElement {
@state()
private data: appstate.IDataState = { secretGroups: [], secretBundles: [] } as any;
constructor() {
super();
const subecription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subecription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>S3</cloudly-sectionheading>
<dees-table
.heading1=${'S3'}
.heading2=${'decoded in client'}
.data=${this.data.s3}
.displayFunction=${(itemArg: plugins.interfaces.data.ICluster) => { return { id: itemArg.id, serverAmount: itemArg.data.servers.length }; }}
.dataActions=${[
{ name: 'add configBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add ConfigBundle', content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
</dees-form>
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-s3': CloudlyViewS3; } }

View File

@@ -0,0 +1,76 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-secretbundles')
export class CloudlyViewSecretBundles extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
constructor() {
super();
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subscription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>SecretBundles</cloudly-sectionheading>
<dees-table
.heading1=${'SecretBundles'}
.heading2=${'decoded in client'}
.data=${this.data.secretBundles || []}
.displayFunction=${(itemArg: plugins.interfaces.data.ISecretBundle) => {
return {
name: itemArg.data.name,
secretGroups: (() => {
const secretGroupIds = itemArg.data.includedSecretGroupIds;
let secretGroupNames: string[] = [];
for (const secretGroupId of secretGroupIds) {
const secretGroup = this.data.secretGroups.find((secretGroupArg: any) => secretGroupArg.id === secretGroupId);
if (secretGroup) { secretGroupNames.push(secretGroup.data.name); }
}
return secretGroupNames.join(', ');
})(),
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${itemArg.data.includedTags}></dees-chips>`,
};
}}
.dataActions=${[
{ name: 'add SecretBundle', iconName: 'plus', type: ['header', 'footer'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Add SecretBundle', content: html`
<dees-form>
<dees-input-text .key=${'id'} .label=${'ID'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.secretGroupIds'} .label=${'secretGroupIds'} .value=${''}></dees-input-text>
<dees-input-text .key=${'data.includedTags'} .label=${'includedTags'} .value=${''}></dees-input-text>
</dees-form>
`, menuOptions: [ { name: 'create', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (actionDataArg: any) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ConfigBundle ${actionDataArg.item.id}`, content: html`
<div style="text-align:center">Do you really want to delete the ConfigBundle?</div>
<div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${actionDataArg.item.id}</div>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { appstate.dataState.dispatchAction(appstate.deleteSecretBundleAction, { configBundleId: actionDataArg.item.id, }); await modalArg.destroy(); } } ] });
} },
{ name: 'edit', iconName: 'penToSquare', type: ['doubleClick', 'contextmenu', 'inRow'], actionFunc: async () => {
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit SecretBundle', content: html`<dees-form><dees-input-text .label=${'purpose'}></dees-input-text></dees-form>`, menuOptions: [ { name: 'save', action: async (modalArg: any) => {} }, { name: 'cancel', action: async (modalArg: any) => { modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretbundles': CloudlyViewSecretBundles; } }

View File

@@ -0,0 +1,77 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import { DeesElement, customElement, html, state, css, cssManager } from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-secretsgroups')
export class CloudlyViewSecretGroups extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
constructor() {
super();
const subscription = appstate.dataState.select((stateArg) => stateArg).subscribe((dataArg) => { this.data = dataArg; });
this.rxSubscriptions.push(subscription);
}
public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css`` ];
public render() {
return html`
<cloudly-sectionheading>SecretGroups</cloudly-sectionheading>
<dees-table
heading1="SecretGroups"
heading2="decoded in client"
.data=${this.data.secretGroups || []}
.displayFunction=${(secretGroup: plugins.interfaces.data.ISecretGroup) => {
return {
name: secretGroup.data.name,
priority: secretGroup.data.priority,
tags: html`<dees-chips .selectionMode=${'none'} .selectableChips=${secretGroup.data.tags}></dees-chips>`,
key: secretGroup.data.key,
history: (() => { const allHistory = []; for (const environment in secretGroup.data.environments) { allHistory.push(...secretGroup.data.environments[environment].history); } return allHistory.length; })(),
};
}}
.dataActions=${[
{ name: 'add SecretGroup', type: ['header', 'footer'], iconName: 'plus', actionFunc: async () => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: 'create new SecretGroup', content: html`
<dees-form>
<dees-input-text .label=${'name'} .key=${'data.name'} .value=${''}></dees-input-text>
<dees-input-text .label=${'description'} .key=${'data.description'} .value=${''}></dees-input-text>
<dees-input-text .label=${'Secret Key (data.key)'} .key=${'data.key'} .value=${''}></dees-input-text>
<dees-table heading1=${'Environments'} heading2=${'keys need to be unique'} key="environments" .data=${[{ environment: 'production', value: '' }, { environment: 'staging', value: '' }]} .dataActions=${[{ name: 'add environment', iconName: 'plus', type: ['footer'], actionFunc: async (dataArg: any) => { dataArg.table.data.push({ environment: 'new environment', value: '' }); dataArg.table.requestUpdate('data'); } }, { name: 'delete environment', iconName: 'trash', type: ['inRow'], actionFunc: async (dataArg: any) => { dataArg.table.data.splice(dataArg.table.data.indexOf(dataArg.item), 1); dataArg.table.requestUpdate('data'); } }] as plugins.deesCatalog.ITableAction[]} .editableFields=${['environment', 'value']}>
</dees-table>
</dees-form>
`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'save', action: async (modalArg: any) => { const deesForm = modalArg.shadowRoot.querySelector('dees-form'); const formData = await deesForm.collectFormData(); const environments: plugins.interfaces.data.ISecretGroup['data']['environments'] = {}; for (const itemArg of formData['environments'] as any[]) { environments[itemArg.environment] = { value: itemArg.value, history: [], lastUpdated: Date.now(), }; } await appstate.dataState.dispatchAction(appstate.createSecretGroupAction, { id: null, data: { name: formData['data.name'] as string, description: formData['data.description'] as string, key: formData['data.key'] as string, environments, tags: [], }, }); await modalArg.destroy(); } } ] });
} },
{ name: 'edit', type: ['contextmenu', 'inRow', 'doubleClick'], iconName: 'penToSquare', actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
const environmentsArray: Array<plugins.interfaces.data.ISecretGroup['data']['environments'][any] & { environment: string; }> = [];
for (const environmentName of Object.keys(dataArg.item.data.environments)) { environmentsArray.push({ environment: environmentName, ...dataArg.item.data.environments[environmentName], }); }
await plugins.deesCatalog.DeesModal.createAndShow({ heading: 'Edit Secret', content: html`
<dees-form>
<dees-input-text .key=${'id'} .disabled=${true} .label=${'ID'} .value=${dataArg.item.id}></dees-input-text>
<dees-input-text .key=${'data.name'} .disabled=${false} .label=${'name'} .value=${dataArg.item.data.name}></dees-input-text>
<dees-input-text .key=${'data.description'} .disabled=${false} .label=${'description'} .value=${dataArg.item.data.description}></dees-input-text>
<dees-input-text .key=${'data.key'} .disabled=${false} .label=${'key'} .value=${dataArg.item.data.key}></dees-input-text>
<dees-table .key=${'environments'} .heading1=${'Environments'} .heading2=${'double-click to edit values'} .data=${environmentsArray.map((itemArg) => ({ environment: itemArg.environment, value: itemArg.value, }))} .editableFields=${['environment', 'value']} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['inRow'], actionFunc: async (actionDataArg: any) => { actionDataArg.table.data.splice(actionDataArg.table.data.indexOf(actionDataArg.item), 1); } }] as plugins.deesCatalog.ITableAction[]}>
</dees-table>
</dees-form>
`, menuOptions: [ { name: 'Cancel', iconName: null, action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'Save', iconName: null, action: async (modalArg: any) => { const data = await modalArg.shadowRoot.querySelector('dees-form').collectFormData(); console.log(data); } } ] });
} },
{ name: 'history', iconName: 'clockRotateLeft', type: ['contextmenu', 'inRow'], actionFunc: async (dataArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
const historyArray: Array<{ environment: string; value: string; }> = []; for (const environment of Object.keys(dataArg.item.data.environments)) { for (const historyItem of dataArg.item.data.environments[environment].history) { historyArray.push({ environment, value: historyItem.value, }); } }
await plugins.deesCatalog.DeesModal.createAndShow({ heading: `history for ${dataArg.item.data.key}`, content: html`<dees-table .data=${historyArray} .dataActions=${[{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<(typeof historyArray)[0]>) => { console.log('delete', itemArg); }, }] as plugins.deesCatalog.ITableAction[]}></dees-table>`, menuOptions: [ { name: 'close', action: async (modalArg: any) => { await modalArg.destroy(); } } ] });
} },
{ name: 'delete', iconName: 'trash', type: ['contextmenu', 'inRow'], actionFunc: async (itemArg: plugins.deesCatalog.ITableActionDataArg<plugins.interfaces.data.ISecretGroup>) => {
plugins.deesCatalog.DeesModal.createAndShow({ heading: `Delete ${itemArg.item.data.key}`, content: html`<div style="text-align:center">Do you really want to delete the secret?</div><div style="font-size: 0.8em; color: red; text-align:center; padding: 16px; margin-top: 24px; border: 1px solid #444; font-family: Intel One Mono; font-size: 16px;">${itemArg.item.data.key}</div>`, menuOptions: [ { name: 'cancel', action: async (modalArg: any) => { await modalArg.destroy(); } }, { name: 'delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteSecretGroupAction, { secretGroupId: itemArg.item.id, }); await modalArg.destroy(); } } ] });
} },
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
declare global { interface HTMLElementTagNameMap { 'cloudly-view-secretsgroups': CloudlyViewSecretGroups; } }

View File

@@ -1,5 +1,5 @@
import * as plugins from '../plugins.js'; import * as plugins from '../../../plugins.js';
import * as shared from '../elements/shared/index.js'; import * as shared from '../../shared/index.js';
import { import {
DeesElement, DeesElement,
@@ -10,15 +10,12 @@ import {
cssManager, cssManager,
} from '@design.estate/dees-element'; } from '@design.estate/dees-element';
import * as appstate from '../appstate.js'; import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-services') @customElement('cloudly-view-services')
export class CloudlyViewServices extends DeesElement { export class CloudlyViewServices extends DeesElement {
@state() @state()
private data: appstate.IDataState = { private data: appstate.IDataState = {} as any;
secretGroups: [],
secretBundles: [],
};
constructor() { constructor() {
super(); super();
@@ -34,45 +31,20 @@ export class CloudlyViewServices extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
shared.viewHostCss, shared.viewHostCss,
css` css`
.category-badge { .category-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 500; }
padding: 2px 8px; .category-base { background: #2196f3; color: white; }
border-radius: 4px; .category-distributed { background: #9c27b0; color: white; }
font-size: 0.9em; .category-workload { background: #4caf50; color: white; }
font-weight: 500; .strategy-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; background: #444; color: #ccc; margin-left: 4px; }
}
.category-base {
background: #2196f3;
color: white;
}
.category-distributed {
background: #9c27b0;
color: white;
}
.category-workload {
background: #4caf50;
color: white;
}
.strategy-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
background: #444;
color: #ccc;
margin-left: 4px;
}
`, `,
]; ];
private getCategoryIcon(category: string): string { private getCategoryIcon(category: string): string {
switch (category) { switch (category) {
case 'base': case 'base': return 'lucide:ServerCog';
return 'lucide:ServerCog'; case 'distributed': return 'lucide:Network';
case 'distributed': case 'workload': return 'lucide:Container';
return 'lucide:Network'; default: return 'lucide:Box';
case 'workload':
return 'lucide:Container';
default:
return 'lucide:Box';
} }
} }
@@ -113,70 +85,28 @@ export class CloudlyViewServices extends DeesElement {
name: 'Add Service', name: 'Add Service',
iconName: 'plus', iconName: 'plus',
type: ['header', 'footer'], type: ['header', 'footer'],
actionFunc: async (dataActionArg) => { actionFunc: async () => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({ const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Service', heading: 'Add Service',
content: html` content: html`
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Service Name'} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text> <dees-input-text .key=${'description'} .label=${'Description'} .required=${true}></dees-input-text>
<dees-input-dropdown <dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} .value=${'workload'} .required=${true}></dees-input-dropdown>
.key=${'serviceCategory'} <dees-input-dropdown .key=${'deploymentStrategy'} .label=${'Deployment Strategy'} .options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]} .value=${'custom'} .required=${true}></dees-input-dropdown>
.label=${'Service Category'} <dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${'1'} .type=${'number'}></dees-input-text>
.options=${['base', 'distributed', 'workload']} <dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${false}></dees-input-checkbox>
.value=${'workload'}
.required=${true}>
</dees-input-dropdown>
<dees-input-dropdown
.key=${'deploymentStrategy'}
.label=${'Deployment Strategy'}
.options=${['all-nodes', 'limited-replicas', 'custom']}
.value=${'custom'}
.required=${true}>
</dees-input-dropdown>
<dees-input-text
.key=${'maxReplicas'}
.label=${'Max Replicas (for distributed services)'}
.value=${'3'}
.type=${'number'}>
</dees-input-text>
<dees-input-checkbox
.key=${'antiAffinity'}
.label=${'Enable Anti-Affinity'}
.value=${false}>
</dees-input-checkbox>
<dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text> <dees-input-text .key=${'imageId'} .label=${'Image ID'} .required=${true}></dees-input-text>
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text> <dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text>
<dees-input-text <dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${'1'} .type=${'number'} .required=${true}></dees-input-text>
.key=${'scaleFactor'} <dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${'round-robin'} .required=${true}></dees-input-dropdown>
.label=${'Scale Factor'} <dees-input-text .key=${'webPort'} .label=${'Web Port'} .value=${'80'} .type=${'number'} .required=${true}></dees-input-text>
.value=${'1'}
.type=${'number'}
.required=${true}>
</dees-input-text>
<dees-input-dropdown
.key=${'balancingStrategy'}
.label=${'Balancing Strategy'}
.options=${['round-robin', 'least-connections']}
.value=${'round-robin'}
.required=${true}>
</dees-input-dropdown>
<dees-input-text
.key=${'webPort'}
.label=${'Web Port'}
.value=${'80'}
.type=${'number'}
.required=${true}>
</dees-input-text>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [
{ { name: 'Create Service', action: async (modalArg: any) => {
name: 'Create Service',
action: async (modalArg) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any; const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData(); const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.createServiceAction, { await appstate.dataState.dispatchAction(appstate.createServiceAction, {
serviceData: { serviceData: {
name: formData.name, name: formData.name,
@@ -189,24 +119,15 @@ export class CloudlyViewServices extends DeesElement {
imageVersion: formData.imageVersion, imageVersion: formData.imageVersion,
scaleFactor: parseInt(formData.scaleFactor), scaleFactor: parseInt(formData.scaleFactor),
balancingStrategy: formData.balancingStrategy, balancingStrategy: formData.balancingStrategy,
ports: { ports: { web: parseInt(formData.webPort) },
web: parseInt(formData.webPort),
},
environment: {}, environment: {},
domains: [], domains: [],
deploymentIds: [], deploymentIds: [],
}, },
}); });
await modalArg.destroy(); await modalArg.destroy();
}, }},
}, { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
], ],
}); });
}, },
@@ -215,7 +136,7 @@ export class CloudlyViewServices extends DeesElement {
name: 'Edit', name: 'Edit',
iconName: 'edit', iconName: 'edit',
type: ['contextmenu', 'inRow'], type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => { actionFunc: async (actionDataArg: any) => {
const service = actionDataArg.item as plugins.interfaces.data.IService; const service = actionDataArg.item as plugins.interfaces.data.IService;
const modal = await plugins.deesCatalog.DeesModal.createAndShow({ const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Edit Service: ${service.data.name}`, heading: `Edit Service: ${service.data.name}`,
@@ -223,55 +144,19 @@ export class CloudlyViewServices extends DeesElement {
<dees-form> <dees-form>
<dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text> <dees-input-text .key=${'name'} .label=${'Service Name'} .value=${service.data.name} .required=${true}></dees-input-text>
<dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text> <dees-input-text .key=${'description'} .label=${'Description'} .value=${service.data.description} .required=${true}></dees-input-text>
<dees-input-dropdown <dees-input-dropdown .key=${'serviceCategory'} .label=${'Service Category'} .options=${[{key: 'base', option: 'Base'}, {key: 'distributed', option: 'Distributed'}, {key: 'workload', option: 'Workload'}]} .value=${service.data.serviceCategory || 'workload'} .required=${true}></dees-input-dropdown>
.key=${'serviceCategory'} <dees-input-dropdown .key=${'deploymentStrategy'} .label=${'Deployment Strategy'} .options=${[{key: 'all-nodes', option: 'All Nodes'}, {key: 'limited-replicas', option: 'Limited Replicas'}, {key: 'custom', option: 'Custom'}]} .value=${service.data.deploymentStrategy || 'custom'} .required=${true}></dees-input-dropdown>
.label=${'Service Category'} <dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${service.data.maxReplicas || ''} .type=${'number'}></dees-input-text>
.options=${['base', 'distributed', 'workload']} <dees-input-checkbox .key=${'antiAffinity'} .label=${'Enable Anti-Affinity'} .value=${service.data.antiAffinity || false}></dees-input-checkbox>
.value=${service.data.serviceCategory || 'workload'}
.required=${true}>
</dees-input-dropdown>
<dees-input-dropdown
.key=${'deploymentStrategy'}
.label=${'Deployment Strategy'}
.options=${['all-nodes', 'limited-replicas', 'custom']}
.value=${service.data.deploymentStrategy || 'custom'}
.required=${true}>
</dees-input-dropdown>
<dees-input-text
.key=${'maxReplicas'}
.label=${'Max Replicas (for distributed services)'}
.value=${service.data.maxReplicas || ''}
.type=${'number'}>
</dees-input-text>
<dees-input-checkbox
.key=${'antiAffinity'}
.label=${'Enable Anti-Affinity'}
.value=${service.data.antiAffinity || false}>
</dees-input-checkbox>
<dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text> <dees-input-text .key=${'imageVersion'} .label=${'Image Version'} .value=${service.data.imageVersion} .required=${true}></dees-input-text>
<dees-input-text <dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${service.data.scaleFactor} .type=${'number'} .required=${true}></dees-input-text>
.key=${'scaleFactor'} <dees-input-dropdown .key=${'balancingStrategy'} .label=${'Balancing Strategy'} .options=${[{key: 'round-robin', option: 'Round Robin'}, {key: 'least-connections', option: 'Least Connections'}]} .value=${service.data.balancingStrategy} .required=${true}></dees-input-dropdown>
.label=${'Scale Factor'}
.value=${service.data.scaleFactor}
.type=${'number'}
.required=${true}>
</dees-input-text>
<dees-input-dropdown
.key=${'balancingStrategy'}
.label=${'Balancing Strategy'}
.options=${['round-robin', 'least-connections']}
.value=${service.data.balancingStrategy}
.required=${true}>
</dees-input-dropdown>
</dees-form> </dees-form>
`, `,
menuOptions: [ menuOptions: [
{ { name: 'Update Service', action: async (modalArg: any) => {
name: 'Update Service',
action: async (modalArg) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any; const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData(); const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.updateServiceAction, { await appstate.dataState.dispatchAction(appstate.updateServiceAction, {
serviceId: service.id, serviceId: service.id,
serviceData: { serviceData: {
@@ -287,16 +172,9 @@ export class CloudlyViewServices extends DeesElement {
balancingStrategy: formData.balancingStrategy, balancingStrategy: formData.balancingStrategy,
}, },
}); });
await modalArg.destroy(); await modalArg.destroy();
}, }},
}, { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{
name: 'Cancel',
action: async (modalArg) => {
modalArg.destroy();
},
},
], ],
}); });
}, },
@@ -305,9 +183,8 @@ export class CloudlyViewServices extends DeesElement {
name: 'Deploy', name: 'Deploy',
iconName: 'rocket', iconName: 'rocket',
type: ['contextmenu', 'inRow'], type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => { actionFunc: async (actionDataArg: any) => {
const service = actionDataArg.item as plugins.interfaces.data.IService; const service = actionDataArg.item as plugins.interfaces.data.IService;
// TODO: Implement deployment action
console.log('Deploy service:', service); console.log('Deploy service:', service);
}, },
}, },
@@ -315,38 +192,21 @@ export class CloudlyViewServices extends DeesElement {
name: 'Delete', name: 'Delete',
iconName: 'trash', iconName: 'trash',
type: ['contextmenu', 'inRow'], type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg) => { actionFunc: async (actionDataArg: any) => {
const service = actionDataArg.item as plugins.interfaces.data.IService; const service = actionDataArg.item as plugins.interfaces.data.IService;
plugins.deesCatalog.DeesModal.createAndShow({ plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Service: ${service.data.name}`, heading: `Delete Service: ${service.data.name}`,
content: html` content: html`
<div style="text-align:center"> <div style="text-align:center">Are you sure you want to delete this service?</div>
Are you sure you want to delete this service?
</div>
<div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;"> <div style="margin-top: 16px; padding: 16px; background: #333; border-radius: 8px;">
<div style="color: #fff; font-weight: bold;">${service.data.name}</div> <div style="color: #fff; font-weight: bold;">${service.data.name}</div>
<div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div> <div style="color: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div>
<div style="color: #f44336; margin-top: 8px;"> <div style="color: #f44336; margin-top: 8px;">This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)</div>
This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)
</div>
</div> </div>
`, `,
menuOptions: [ menuOptions: [
{ { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
name: 'Cancel', { name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { serviceId: service.id }); await modalArg.destroy(); } },
action: async (modalArg) => {
await modalArg.destroy();
},
},
{
name: 'Delete',
action: async (modalArg) => {
await appstate.dataState.dispatchAction(appstate.deleteServiceAction, {
serviceId: service.id,
});
await modalArg.destroy();
},
},
], ],
}); });
}, },
@@ -356,3 +216,10 @@ export class CloudlyViewServices extends DeesElement {
`; `;
} }
} }
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-services': CloudlyViewServices;
}
}

View File

@@ -0,0 +1,206 @@
import * as plugins from '../../../plugins.js';
import * as shared from '../../shared/index.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
@customElement('cloudly-view-settings')
export class CloudlyViewSettings extends DeesElement {
@state()
private settings: plugins.interfaces.data.ICloudlySettingsMasked = {} as any;
@state()
private isLoading = false;
@state()
private testResults: {[key: string]: {success: boolean; message: string}} = {};
constructor() {
super();
this.loadSettings();
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.settings-container { padding: 24px 0; display: flex; flex-direction: column; gap: 16px; }
.provider-icon { margin-right: 8px; font-size: 20px; }
.test-status { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.test-status dees-button { margin-left: auto; }
.loading-container { display: flex; justify-content: center; padding: 48px; }
.actions-container { display: flex; justify-content: center; margin-top: 24px; }
dees-panel { margin-bottom: 16px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.form-grid.single { grid-template-columns: 1fr; }
@media (max-width: 768px) { .form-grid { grid-template-columns: 1fr; } }
`,
];
private async loadSettings() {
this.isLoading = true;
try {
const response = await appstate.apiClient.settings.getSettings();
this.settings = response.settings;
} catch (error: any) {
console.error('Failed to load settings:', error);
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to load settings: ${error.message}`, type: 'error' });
} finally {
this.isLoading = false;
}
}
private async saveSettings(formData: any) {
this.isLoading = true;
try {
const updates: Partial<plugins.interfaces.data.ICloudlySettings> = {};
for (const [key, value] of Object.entries(formData)) {
if (value !== undefined && value !== '****' && !value?.toString().endsWith('****')) {
updates[key as keyof plugins.interfaces.data.ICloudlySettings] = value as string;
}
}
const response = await appstate.apiClient.settings.updateSettings(updates);
if (response.success) {
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Settings saved successfully', type: 'success' });
await this.loadSettings();
} else {
throw new Error(response.message);
}
} catch (error: any) {
console.error('Failed to save settings:', error);
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to save settings: ${error.message}`, type: 'error' });
} finally {
this.isLoading = false;
}
}
private async testConnection(provider: string) {
this.isLoading = true;
try {
const response = await appstate.apiClient.settings.testProviderConnection(provider);
this.testResults = { ...this.testResults, [provider]: { success: response.connectionValid, message: response.message } };
plugins.deesCatalog.DeesToast.createAndShow({ message: response.message, type: response.connectionValid ? 'success' : 'error' });
} catch (error: any) {
this.testResults = { ...this.testResults, [provider]: { success: false, message: `Test failed: ${error.message}` } };
plugins.deesCatalog.DeesToast.createAndShow({ message: `Connection test failed: ${error.message}`, type: 'error' });
} finally {
this.isLoading = false;
}
}
private renderProviderStatus(provider: string) {
const result = this.testResults[provider];
if (!result) return '' as any;
return html`<dees-badge .type=${result.success ? 'success' : 'error'} .text=${result.success ? 'Connected' : 'Failed'}></dees-badge>`;
}
public render() {
if (this.isLoading && Object.keys(this.settings).length === 0) {
return html`<div class="loading-container"><dees-spinner></dees-spinner></div>`;
}
return html`
<cloudly-sectionheading>Settings</cloudly-sectionheading>
<div class="settings-container">
<dees-form @formData=${(e: CustomEvent) => { this.saveSettings((e.detail as any).data); }}>
<dees-panel .title=${'Hetzner Cloud'} .subtitle=${'Configure Hetzner Cloud API access'} .variant=${'outline'}>
<div class="test-status">
${this.renderProviderStatus('hetzner')}
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('hetzner'); }}></dees-button>
</div>
<div class="form-grid single">
<dees-input-text .key=${'hetznerToken'} .label=${'API Token'} .value=${this.settings.hetznerToken || ''} .isPasswordBool=${true} .description=${'Your Hetzner Cloud API token for managing infrastructure'} .required=${false}></dees-input-text>
</div>
</dees-panel>
<dees-panel .title=${'Cloudflare'} .subtitle=${'Configure Cloudflare API access'} .variant=${'outline'}>
<div class="test-status">
${this.renderProviderStatus('cloudflare')}
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('cloudflare'); }}></dees-button>
</div>
<div class="form-grid single">
<dees-input-text .key=${'cloudflareToken'} .label=${'API Token'} .value=${this.settings.cloudflareToken || ''} .isPasswordBool=${true} .description=${'Cloudflare API token with DNS and Zone permissions'} .required=${false}></dees-input-text>
</div>
</dees-panel>
<dees-panel .title=${'Amazon Web Services'} .subtitle=${'Configure AWS credentials'} .variant=${'outline'}>
<div class="test-status">
${this.renderProviderStatus('aws')}
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('aws'); }}></dees-button>
</div>
<div class="form-grid">
<dees-input-text .key=${'awsAccessKey'} .label=${'Access Key ID'} .value=${this.settings.awsAccessKey || ''} .isPasswordBool=${true} .description=${'AWS IAM access key identifier'} .required=${false}></dees-input-text>
<dees-input-text .key=${'awsSecretKey'} .label=${'Secret Access Key'} .value=${this.settings.awsSecretKey || ''} .isPasswordBool=${true} .description=${'AWS IAM secret access key'} .required=${false}></dees-input-text>
</div>
<div class="form-grid single">
<dees-input-dropdown .key=${'awsRegion'} .label=${'Default Region'} .selectedOption=${this.settings.awsRegion || 'us-east-1'} .options=${[
{ key: 'us-east-1', option: 'US East (N. Virginia)', payload: null },
{ key: 'us-west-2', option: 'US West (Oregon)', payload: null },
{ key: 'eu-west-1', option: 'EU (Ireland)', payload: null },
{ key: 'eu-central-1', option: 'EU (Frankfurt)', payload: null },
{ key: 'ap-southeast-1', option: 'Asia Pacific (Singapore)', payload: null },
{ key: 'ap-northeast-1', option: 'Asia Pacific (Tokyo)', payload: null },
]} .description=${'Default AWS region for resource provisioning'}></dees-input-dropdown>
</div>
</dees-panel>
<dees-panel .title=${'DigitalOcean'} .subtitle=${'Configure DigitalOcean API access'} .variant=${'outline'}>
<div class="test-status">
${this.renderProviderStatus('digitalocean')}
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('digitalocean'); }}></dees-button>
</div>
<div class="form-grid single">
<dees-input-text .key=${'digitalOceanToken'} .label=${'Personal Access Token'} .value=${this.settings.digitalOceanToken || ''} .isPasswordBool=${true} .description=${'DigitalOcean personal access token with read/write scope'} .required=${false}></dees-input-text>
</div>
</dees-panel>
<dees-panel .title=${'Microsoft Azure'} .subtitle=${'Configure Azure service principal'} .variant=${'outline'}>
<div class="test-status">
${this.renderProviderStatus('azure')}
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('azure'); }}></dees-button>
</div>
<div class="form-grid">
<dees-input-text .key=${'azureClientId'} .label=${'Application (Client) ID'} .value=${this.settings.azureClientId || ''} .isPasswordBool=${true} .description=${'Azure AD application client ID'} .required=${false}></dees-input-text>
<dees-input-text .key=${'azureClientSecret'} .label=${'Client Secret'} .value=${this.settings.azureClientSecret || ''} .isPasswordBool=${true} .description=${'Azure AD application client secret'} .required=${false}></dees-input-text>
</div>
<div class="form-grid">
<dees-input-text .key=${'azureTenantId'} .label=${'Directory (Tenant) ID'} .value=${this.settings.azureTenantId || ''} .description=${'Azure AD tenant identifier'} .required=${false}></dees-input-text>
<dees-input-text .key=${'azureSubscriptionId'} .label=${'Subscription ID'} .value=${this.settings.azureSubscriptionId || ''} .description=${'Azure subscription for resource management'} .required=${false}></dees-input-text>
</div>
</dees-panel>
<dees-panel .title=${'Google Cloud Platform'} .subtitle=${'Configure GCP service account'} .variant=${'outline'}>
<div class="test-status">
${this.renderProviderStatus('google')}
<dees-button .text=${'Test Connection'} .type=${'secondary'} @click=${(e: Event) => { e.preventDefault(); e.stopPropagation(); this.testConnection('google'); }}></dees-button>
</div>
<div class="form-grid single">
<dees-input-textarea .key=${'googleCloudKeyJson'} .label=${'Service Account Key (JSON)'} .value=${this.settings.googleCloudKeyJson || ''} .isPasswordBool=${true} .description=${'Complete JSON key file for service account authentication'} .required=${false}></dees-input-textarea>
</div>
<div class="form-grid single">
<dees-input-text .key=${'googleCloudProjectId'} .label=${'Project ID'} .value=${this.settings.googleCloudProjectId || ''} .description=${'Google Cloud project identifier'} .required=${false}></dees-input-text>
</div>
</dees-panel>
<div class="actions-container">
<dees-form-submit .text=${'Save All Settings'} .disabled=${this.isLoading}></dees-form-submit>
</div>
</dees-form>
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-settings': CloudlyViewSettings;
}
}

View File

@@ -0,0 +1,308 @@
import * as shared from '../../shared/index.js';
import * as plugins from '../../../plugins.js';
import {
DeesElement,
customElement,
html,
state,
css,
cssManager,
} from '@design.estate/dees-element';
import * as appstate from '../../../appstate.js';
import './parts/cloudly-task-panel.js';
import './parts/cloudly-execution-details.js';
import { formatCronFriendly, formatDate, formatDuration } from './utils.js';
@customElement('cloudly-view-tasks')
export class CloudlyViewTasks extends DeesElement {
@state()
private data: appstate.IDataState = {} as any;
@state()
private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null;
@state()
private loading = false;
@state()
private filterStatus: string = 'all';
@state()
private searchQuery: string = '';
@state()
private categoryFilter: string = 'all';
@state()
private autoRefresh: boolean = true;
private _refreshHandle: any = null;
@state()
private canceling: Record<string, boolean> = {};
constructor() {
super();
const subscription = appstate.dataState
.select((stateArg) => stateArg)
.subscribe((dataArg) => {
this.data = dataArg;
});
this.rxSubscriptions.push(subscription);
// Load initial data (non-blocking)
this.loadInitialData();
// Start periodic refresh (lightweight; executions only by default)
this.startAutoRefresh();
}
private async loadInitialData() {
try {
await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {});
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {});
} catch (error) {
console.error('Failed to load initial task data:', error);
}
}
private startAutoRefresh() {
this.stopAutoRefresh();
if (!this.autoRefresh) return;
this._refreshHandle = setInterval(async () => {
try {
await this.loadExecutionsWithFilter();
} catch (err) {
// ignore transient errors during refresh
}
}, 5000);
}
private stopAutoRefresh() {
if (this._refreshHandle) {
clearInterval(this._refreshHandle);
this._refreshHandle = null;
}
}
public async disconnectedCallback(): Promise<void> {
await (super.disconnectedCallback?.());
this.stopAutoRefresh();
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.toolbar { display: flex; gap: 12px; align-items: center; margin: 4px 0 16px 0; flex-wrap: wrap; }
.toolbar .spacer { flex: 1 1 auto; }
.search-input { background: #111; color: #ddd; border: 1px solid #333; border-radius: 6px; padding: 8px 10px; min-width: 220px; }
.chipbar { display: flex; gap: 8px; flex-wrap: wrap; }
.chip { padding: 6px 10px; background: #2a2a2a; color: #bbb; border: 1px solid #444; border-radius: 16px; cursor: pointer; transition: all 0.2s; user-select: none; }
.chip.active { background: #2196f3; border-color: #2196f3; color: white; }
.task-list { display: flex; flex-direction: column; gap: 16px; margin-bottom: 32px; }
.secondary-button { padding: 6px 12px; background: #2b2b2b; color: #ccc; border: 1px solid #444; border-radius: 6px; cursor: pointer; font-size: 0.9em; transition: background 0.2s, border-color 0.2s; }
.secondary-button:hover { background: #363636; border-color: #555; }
/* Shared badge styles used within the table content */
.status-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.85em; font-weight: 500; }
.status-running { background: #2196f3; color: white; }
.status-completed { background: #4caf50; color: white; }
.status-failed { background: #f44336; color: white; }
.status-cancelled { background: #ff9800; color: white; }
.execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; }
.log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; }
.log-info { color: #2196f3; }
.log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
.log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); }
.log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
`,
];
private async triggerTask(taskName: string) {
try {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Run Task: ${taskName}`,
content: html`<div><p>Do you want to trigger this task now?</p></div>`,
menuOptions: [
{
name: 'Run now',
action: async (modalArg: any) => {
await appstate.dataState.dispatchAction(appstate.taskActions.triggerTask, { taskName });
plugins.deesCatalog.DeesToast.createAndShow({ message: `Task ${taskName} triggered`, type: 'success' });
await modalArg.destroy();
await this.loadExecutionsWithFilter();
}
},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() }
]
});
} catch (error) {
console.error('Failed to trigger task:', error);
plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error.message}`, type: 'error' });
}
}
private async cancelTaskFor(taskName: string) {
try {
const executions = (this.data.taskExecutions || [])
.filter((e: any) => e.data.taskName === taskName && e.data.status === 'running')
.sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0));
const running = executions[0];
if (!running) return;
this.canceling = { ...this.canceling, [running.id]: true };
try {
await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: running.id });
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancelled ${taskName}`, type: 'success' });
} finally {
this.canceling = { ...this.canceling, [running.id]: false };
await this.loadExecutionsWithFilter();
}
} catch (err) {
console.error('Failed to cancel task:', err);
plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error' });
}
}
private async loadExecutionsWithFilter() {
try {
const filter: any = {};
if (this.filterStatus !== 'all') {
filter.status = this.filterStatus;
}
await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, { filter });
} catch (error) {
console.error('Failed to load executions:', error);
}
}
private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) {
this.selectedExecution = execution;
requestAnimationFrame(() => {
this.shadowRoot?.querySelector('cloudly-sectionheading + cloudly-execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
});
}
private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) {
await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Logs: ${execution.data.taskName}`,
content: html`
<div class="execution-logs">
${(execution.data.logs || []).map((log: any) => html`
<div class="log-entry log-${log.severity}"><span>${formatDate(log.timestamp)}</span> - ${log.message}</div>
`)}
</div>
`,
menuOptions: [
{
name: 'Copy All',
action: async (modalArg: any) => {
try {
await navigator.clipboard.writeText((execution.data.logs || [])
.map((l: any) => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n'));
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' });
} catch (e) {
plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' });
}
}
},
{ name: 'Close', action: async (modalArg: any) => modalArg.destroy() }
]
});
}
public render() {
const tasks = (this.data.tasks || []) as any[];
const categories = Array.from(new Set(tasks.map(t => t.category))).sort();
const filteredTasks = tasks
.filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter)
.filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase()));
return html`
<cloudly-sectionheading>Tasks</cloudly-sectionheading>
<dees-panel .title=${'Task Library'} .subtitle=${'Run maintenance, monitoring and system tasks'} .variant=${'outline'}>
<div class="toolbar">
<div class="chipbar">
<div class="chip ${this.categoryFilter === 'all' ? 'active' : ''}"
@click=${() => { this.categoryFilter = 'all'; }}>
All
</div>
${categories.map(cat => html`
<div class="chip ${this.categoryFilter === cat ? 'active' : ''}"
@click=${() => { this.categoryFilter = cat; }}>
${cat}
</div>
`)}
</div>
<div class="spacer"></div>
<input class="search-input" placeholder="Search tasks" .value=${this.searchQuery}
@input=${(e: any) => { this.searchQuery = e.target.value; }} />
<button class="secondary-button" @click=${async () => { await this.loadExecutionsWithFilter(); }}>Refresh</button>
<button class="secondary-button" @click=${() => { this.autoRefresh = !this.autoRefresh; this.autoRefresh ? this.startAutoRefresh() : this.stopAutoRefresh(); }}>
${this.autoRefresh ? 'Auto-Refresh: On' : 'Auto-Refresh: Off'}
</button>
</div>
<div class="task-list">
${filteredTasks.map(task => html`
<cloudly-task-panel
.task=${task}
.executions=${this.data.taskExecutions || []}
.canceling=${this.canceling}
.onRun=${(name: string) => this.triggerTask(name)}
.onCancel=${(name: string) => this.cancelTaskFor(name)}
.onOpenDetails=${(exec: any) => this.openExecutionDetails(exec)}
.onOpenLogs=${(exec: any) => this.openLogsModal(exec)}
></cloudly-task-panel>
`)}
</div>
</dees-panel>
<cloudly-sectionheading>Execution History</cloudly-sectionheading>
<dees-panel .title=${'Recent Executions'} .subtitle=${'History of task runs and their outcomes'} .variant=${'outline'}>
<dees-table
.heading1=${'Task Executions'}
.heading2=${'History of task runs and their outcomes'}
.data=${this.data.taskExecutions || []}
.displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => {
return {
Task: itemArg.data.taskName,
Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`,
'Started At': formatDate(itemArg.data.startedAt),
Duration: itemArg.data.duration ? formatDuration(itemArg.data.duration) : '-',
'Triggered By': itemArg.data.triggeredBy,
Logs: itemArg.data.logs?.length || 0,
} as any;
}}
.actionFunction=${async (itemArg: plugins.interfaces.data.ITaskExecution) => {
const actions: any[] = [
{ name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; } }
];
if (itemArg.data.status === 'running') {
actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); } });
}
return actions;
}}
></dees-table>
</dees-panel>
${this.selectedExecution ? html`
<cloudly-sectionheading>Execution Details</cloudly-sectionheading>
<cloudly-execution-details .execution=${this.selectedExecution}></cloudly-execution-details>
` : ''}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-view-tasks': CloudlyViewTasks;
}
}

View File

@@ -0,0 +1,93 @@
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
import { formatDate, formatDuration } from '../utils.js';
@customElement('cloudly-execution-details')
export class CloudlyExecutionDetails extends DeesElement {
@property({ type: Object }) execution: any;
public static styles = [
cssManager.defaultStyles,
css`
.execution-details h3, .execution-details h4 { margin: 8px 0; }
.metrics { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid #333; }
.metric { display: flex; flex-direction: column; }
.metric-label { color: #666; font-size: 0.85em; }
.metric-value { color: #fff; font-size: 1.1em; font-weight: 600; }
.execution-logs { background: #0a0a0a; border: 1px solid #333; border-radius: 6px; padding: 16px; margin-top: 16px; max-height: 400px; overflow-y: auto; }
.log-entry { font-family: monospace; font-size: 0.9em; margin-bottom: 8px; padding: 4px 8px; border-radius: 4px; }
.log-info { color: #2196f3; }
.log-warning { color: #ff9800; background: rgba(255, 152, 0, 0.1); }
.log-error { color: #f44336; background: rgba(244, 67, 54, 0.1); }
.log-success { color: #4caf50; background: rgba(76, 175, 80, 0.1); }
`,
];
public render() {
const execution = this.execution;
if (!execution) return html``;
return html`
<div class="execution-details">
<h3>Execution Details: ${execution.data.taskName}</h3>
<div class="metrics">
<div class="metric">
<span class="metric-label">Started</span>
<span class="metric-value">${formatDate(execution.data.startedAt)}</span>
</div>
${execution.data.completedAt ? html`
<div class="metric">
<span class="metric-label">Completed</span>
<span class="metric-value">${formatDate(execution.data.completedAt)}</span>
</div>
` : ''}
${execution.data.duration ? html`
<div class="metric">
<span class="metric-label">Duration</span>
<span class="metric-value">${formatDuration(execution.data.duration)}</span>
</div>
` : ''}
<div class="metric">
<span class="metric-label">Triggered By</span>
<span class="metric-value">${execution.data.triggeredBy}</span>
</div>
</div>
${execution.data.logs && execution.data.logs.length > 0 ? html`
<h4>Logs</h4>
<div class="execution-logs">
${execution.data.logs.map((log: any) => html`
<div class="log-entry log-${log.severity}">
<span>${formatDate(log.timestamp)}</span> - ${log.message}
</div>
`)}
</div>
` : ''}
${execution.data.metrics ? html`
<h4>Metrics</h4>
<div class="metrics">
${Object.entries(execution.data.metrics).map(([key, value]) => html`
<div class="metric">
<span class="metric-label">${key}</span>
<span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span>
</div>
`)}
</div>
` : ''}
${execution.data.error ? html`
<h4>Error</h4>
<div class="execution-logs">
<div class="log-entry log-error">
${execution.data.error.message}
${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''}
</div>
</div>
` : ''}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-execution-details': CloudlyExecutionDetails;
}
}

View File

@@ -0,0 +1,206 @@
import { DeesElement, customElement, html, css, cssManager, property } from '@design.estate/dees-element';
import { formatCronFriendly, formatDuration, formatRelativeTime, getCategoryHue, getCategoryIcon } from '../utils.js';
@customElement('cloudly-task-panel')
export class CloudlyTaskPanel extends DeesElement {
@property({ type: Object }) task: any;
@property({ type: Array }) executions: any[] = [];
@property({ type: Object }) canceling: Record<string, boolean> = {};
// Callbacks provided by parent view
@property({ attribute: false }) onRun?: (taskName: string) => void;
@property({ attribute: false }) onCancel?: (taskName: string) => void;
@property({ attribute: false }) onOpenDetails?: (execution: any) => void;
@property({ attribute: false }) onOpenLogs?: (execution: any) => void;
public static styles = [
cssManager.defaultStyles,
css`
.task-panel {
background: #131313;
border: 1px solid #2a2a2a;
border-radius: 10px;
padding: 16px;
transition: border-color 0.2s, background 0.2s;
}
.task-panel:hover { border-color: #3a3a3a; }
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.header-left { display: flex; align-items: center; gap: 12px; min-width: 0; }
.header-right { display: flex; align-items: center; gap: 8px; }
.task-icon { color: #cfcfcf; font-size: 28px; }
.task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.task-subtitle { color: #8c8c8c; font-size: 0.9em; }
.task-description {
color: #b5b5b5;
font-size: 0.95em;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
margin-top: 8px;
width: 100%;
max-width: 760px;
}
.metric-item {
background: #0f0f0f;
border: 1px solid #2c2c2c;
border-radius: 8px;
padding: 10px 12px;
}
.metric-item .label { color: #8d8d8d; font-size: 0.8em; }
.metric-item .value { color: #eaeaea; font-weight: 600; margin-top: 4px; }
.lastline {
display: flex;
align-items: center;
gap: 8px;
color: #a0a0a0;
font-size: 0.9em;
margin-top: 10px;
}
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.dot.info { background: #2196f3; }
.dot.success { background: #4caf50; }
.dot.warning { background: #ff9800; }
.dot.error { background: #f44336; }
.panel-footer { display: flex; gap: 12px; margin-top: 12px; }
.link-button { background: transparent; border: none; color: #8ab4ff; cursor: pointer; padding: 0; font-size: 0.95em; }
.link-button:hover { text-decoration: underline; }
.status-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 500;
}
.status-running { background: #2196f3; color: white; }
.status-completed { background: #4caf50; color: white; }
.status-failed { background: #f44336; color: white; }
.status-cancelled { background: #ff9800; color: white; }
`,
];
private computeData() {
const task = this.task || {};
const executions = this.executions || [];
const lastExecution = executions
.filter((e: any) => e.data.taskName === task.name)
.sort((a: any, b: any) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0];
const isRunning = lastExecution?.data.status === 'running';
const executionsForTask = executions.filter((e: any) => e.data.taskName === task.name);
const now = Date.now();
const last24hCount = executionsForTask.filter((e: any) => (e.data.startedAt || 0) > now - 86_400_000).length;
const completed = executionsForTask.filter((e: any) => e.data.status === 'completed');
const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0;
const avgDuration = completed.length ? Math.round(completed.reduce((acc: number, e: any) => acc + (e.data.duration || 0), 0) / completed.length) : undefined;
const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null;
const subtitle = [
task.category,
task.schedule ? `${formatCronFriendly(task.schedule)}` : null,
isRunning
? (lastExecution?.data.startedAt ? `Started ${formatRelativeTime(lastExecution.data.startedAt)}` : 'Running')
: (task.lastRun ? `Last ${formatRelativeTime(task.lastRun)}` : 'Never run')
].filter(Boolean).join(' • ');
return { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle };
}
public render() {
const task = this.task;
const { lastExecution, isRunning, last24hCount, successRate, avgDuration, lastLog, subtitle } = this.computeData();
return html`
<div class="task-panel">
<div class="panel-header">
<div class="header-left">
<dees-icon class="task-icon" .icon=${getCategoryIcon(task.category)}></dees-icon>
<div>
<div class="task-name" title=${task.name}>${task.name}</div>
<div class="task-subtitle" title=${task.schedule || ''}>${subtitle}</div>
</div>
</div>
<div class="header-right">
${lastExecution ? html`<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>` : html`<span class="status-badge" style="background:#2e2e2e;color:#ddd;border:1px solid #3a3a3a;">idle</span>`}
${isRunning ? html`
<dees-spinner style="--size: 18px"></dees-spinner>
<dees-button
.text=${this.canceling[lastExecution!.id] ? 'Cancelling…' : 'Cancel'}
.type=${'secondary'}
.disabled=${!!this.canceling[lastExecution!.id]}
@click=${() => this.onCancel?.(task.name)}
></dees-button>
` : html`
<dees-button .text=${'Run'} .type=${'primary'} .disabled=${!task.enabled} @click=${() => this.onRun?.(task.name)}></dees-button>
`}
</div>
</div>
<div class="task-description" title=${task.description || ''}>${task.description}</div>
${lastExecution ? html`
<div class="metrics-grid">
<div class="metric-item">
<div class="label">Last Status</div>
<div class="value">
<span class="status-badge status-${lastExecution.data.status}">${lastExecution.data.status}</span>
</div>
</div>
<div class="metric-item">
<div class="label">Avg Duration</div>
<div class="value">${avgDuration ? formatDuration(avgDuration) : '-'}</div>
</div>
<div class="metric-item">
<div class="label">24h Runs · Success</div>
<div class="value">${last24hCount} · ${successRate}%</div>
</div>
</div>
<div class="lastline">
${lastLog ? html`<span class="dot ${lastLog.severity}"></span> ${lastLog.message}` : 'No recent logs'}
</div>
<div class="panel-footer">
<button class="link-button" @click=${() => this.onOpenDetails?.(lastExecution)}>Details</button>
${lastExecution.data.logs?.length ? html`<button class="link-button" @click=${() => this.onOpenLogs?.(lastExecution)}>Logs</button>` : ''}
</div>
` : html`
<div class="metrics-grid">
<div class="metric-item">
<div class="label">Last Status</div>
<div class="value">—</div>
</div>
<div class="metric-item">
<div class="label">Avg Duration</div>
<div class="value">—</div>
</div>
<div class="metric-item">
<div class="label">24h Runs · Success</div>
<div class="value">0 · 0%</div>
</div>
</div>
`}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'cloudly-task-panel': CloudlyTaskPanel;
}
}

View File

@@ -0,0 +1,68 @@
export function formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleString();
}
export function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
return `${(ms / 3600000).toFixed(1)}h`;
}
export function formatRelativeTime(ts?: number): string {
if (!ts) return '-';
const diff = Date.now() - ts;
const abs = Math.abs(diff);
if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`;
if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`;
if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`;
return `${Math.round(abs / 86_400_000)}d ago`;
}
export function getCategoryIcon(category: string): string {
switch (category) {
case 'maintenance':
return 'lucide:Wrench';
case 'deployment':
return 'lucide:Rocket';
case 'backup':
return 'lucide:Archive';
case 'monitoring':
return 'lucide:Activity';
case 'cleanup':
return 'lucide:Trash2';
case 'system':
return 'lucide:Settings';
case 'security':
return 'lucide:Shield';
default:
return 'lucide:Play';
}
}
export function getCategoryHue(category: string): number {
switch (category) {
case 'maintenance': return 28; // orange
case 'deployment': return 208; // blue
case 'backup': return 122; // green
case 'monitoring': return 280; // purple
case 'cleanup': return 20; // brownish
case 'system': return 200; // steel
case 'security': return 0; // red
default: return 210; // default blue
}
}
export function formatCronFriendly(cron?: string): string {
if (!cron) return '';
const parts = cron.trim().split(/\s+/);
if (parts.length !== 5) return cron; // fallback
const [min, hour, dom, mon, dow] = parts;
if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute';
if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`;
if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`;
if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly';
if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily';
if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly';
return cron;
}

View File

@@ -19,3 +19,7 @@ export {
webjwt, webjwt,
smartstate, smartstate,
} }
// Expose API client so UI can share it with CLI
import * as servezoneApi from '@serve.zone/api';
export { servezoneApi };