Compare commits

..

26 Commits
v5.0.6 ... 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
4e38d2ff43 5.3.0
Some checks failed
Docker (tags) / security (push) Successful in 52s
Docker (tags) / test (push) Failing after 2m4s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2025-09-08 06:46:14 +00:00
e19639c9be feat(web): Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions 2025-09-08 06:46:14 +00:00
c142519004 5.2.0
Some checks failed
Docker (tags) / security (push) Successful in 56s
Docker (tags) / test (push) Failing after 1m55s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2025-09-07 17:21:30 +00:00
54ef62e7af feat(settings): Add runtime settings management, node & baremetal managers, and settings UI 2025-09-07 17:21:30 +00:00
83abe37d8c 5.1.0
Some checks failed
Docker (tags) / security (push) Successful in 53s
Docker (tags) / test (push) Failing after 1m59s
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2025-09-05 16:07:46 +00:00
eefaa55e13 feat(cluster): Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades 2025-09-05 16:07:46 +00:00
98 changed files with 8787 additions and 3096 deletions

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

68
.serena/project.yml Normal file
View File

@@ -0,0 +1,68 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "cloudly"

View File

@@ -1,5 +1,38 @@
# Changelog # Changelog
## 2025-09-08 - 5.3.0 - feat(web)
Add deployments API typings and web UI improvements: services & deployments management with CRUD and actions
- Add deployment request interfaces (ts_interfaces/requests/deployment.ts) to define typed API for create/read/update/delete/scale/restart operations.
- Extend web app state (ts_web/appstate.ts) to include typed services and deployments, and add actions for create/update/delete of services and deployments.
- Enhance web views (ts_web/elements/*): CloudlyViewServices and CloudlyViewDeployments now include richer display, styling, and UI actions (create, edit, deploy, restart, stop, delete).
- Fix subscription variable naming in several web components (subecription -> subscription) and improve table display functions to handle missing data safely.
- Add .claude/settings.local.json (tooling/permissions) used for local development/test tooling.
## 2025-09-07 - 5.2.0 - feat(settings)
Add runtime settings management, node & baremetal managers, and settings UI
- Introduce CloudlySettingsManager to store runtime settings in an EasyStore (MongoDB) with API handlers for get/update/clear/test.
- Add settings data/interface and typedrequest definitions (ts_interfaces/data/settings.ts, ts_interfaces/requests/settings.ts) and expose via interfaces index.
- Add web UI for managing provider credentials and connections (ts_web/elements/cloudly-view-settings.ts) and integrate the Settings view into the dashboard.
- Replace the previous ServerManager concept with NodeManager and BaremetalManager: new ClusterNode and BareMetal models and managers (auto-provisioning / Hetzner integration), plus curlfresh moved to node manager.
- Update Cluster data shape (servers -> nodes) and adjust related code paths (overview stats, cluster creation and provisioning flows).
- Use settingsManager for provider tokens (cloudflareToken, hetznerToken) instead of reading tokens directly from config/env; connector and manager init code updated accordingly.
- Add numerous implementations and API handlers to support baremetal/node lifecycle and control (getBaremetalServers, controlBaremetal, getNodeConfig, node provisioning helpers).
- Reorder Cloudly startup to initialize MongoDB and settings manager before managers that depend on settings; wire settingsManager into Cloudly class.
- Bump package dependency versions for @git.zone/tsdoc, @design.estate/dees-catalog and @push.rocks/taskbuffer in package.json.
## 2025-09-05 - 5.1.0 - feat(cluster)
Add cluster setupMode (manual|hetzner|aws|digitalocean) with conditional Hetzner auto-provisioning; UI and dashboard improvements; dependency upgrades
- Introduce optional setupMode on cluster configs and requests (ICluster.data.setupMode, createCluster request) to allow 'manual' | 'hetzner' | 'aws' | 'digitalocean'.
- ClusterManager: default setupMode to 'manual' when creating clusters and only trigger serverManager.ensureServerInfrastructure() for 'hetzner' clusters.
- ServerManager: skip provisioning for clusters not configured with setupMode 'hetzner' and log skipped clusters.
- Web UI: add a 'Setup Mode' dropdown when creating a cluster so users can choose auto-provisioning provider; ensure the add-cluster action passes setupMode.
- Web UI: dashboard enhancements — add icons to view tabs and replace cluster overview with a stats grid (including total clusters, total servers, images, services, deployments, secret groups/bundles, DNS, DBs, backups, mails, s3). The overview now computes total servers across clusters.
- Package dependency bumps (devDependencies and dependencies) to keep libs up-to-date (examples: @git.zone/tsbuild, @git.zone/tstest, @api.global/typedserver, @apiclient.xyz/docker, @design.estate/dees-catalog, @push.rocks/smartlog, @push.rocks/smartrequest, @push.rocks/taskbuffer, etc.).
- Add .claude/settings.local.json with local Claude permissions (editor/automation config).
## 2025-08-18 - 5.0.6 - fix(connector.letsencrypt) ## 2025-08-18 - 5.0.6 - fix(connector.letsencrypt)
Improve Let's Encrypt integration and certificate handling; fix coreflow certificate response; add local assistant permissions config Improve Let's Encrypt integration and certificate handling; fix coreflow certificate response; add local assistant permissions config

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/cloudly", "name": "@serve.zone/cloudly",
"version": "5.0.6", "version": "5.3.0",
"private": false, "private": false,
"description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.", "description": "A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.",
"type": "module", "type": "module",
@@ -22,24 +22,24 @@
"docs": "tsdoc aidoc" "docs": "tsdoc aidoc"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.7", "@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsdoc": "^1.5.1", "@git.zone/tsdoc": "^1.5.2",
"@git.zone/tspublish": "^1.10.3", "@git.zone/tspublish": "^1.10.3",
"@git.zone/tstest": "^2.3.5", "@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"
}, },
"dependencies": { "dependencies": {
"@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",
"@api.global/typedserver": "^3.0.77", "@api.global/typedserver": "^3.0.79",
"@api.global/typedsocket": "^3.0.1", "@api.global/typedsocket": "^3.0.1",
"@apiclient.xyz/cloudflare": "^6.4.1", "@apiclient.xyz/cloudflare": "^6.4.1",
"@apiclient.xyz/docker": "^1.3.0", "@apiclient.xyz/docker": "^1.3.5",
"@apiclient.xyz/hetznercloud": "^1.2.0", "@apiclient.xyz/hetznercloud": "^1.2.0",
"@apiclient.xyz/slack": "^3.0.9", "@apiclient.xyz/slack": "^3.0.9",
"@design.estate/dees-catalog": "^1.10.10", "@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,21 +57,21 @@
"@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.8", "@push.rocks/smartlog": "^3.1.9",
"@push.rocks/smartlog-destination-clickhouse": "^1.0.13", "@push.rocks/smartlog-destination-clickhouse": "^1.0.13",
"@push.rocks/smartlog-interfaces": "^3.0.2", "@push.rocks/smartlog-interfaces": "^3.0.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.2.2", "@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.0.2", "@push.rocks/taskbuffer": "^3.4.0",
"@push.rocks/webjwt": "^1.0.9", "@push.rocks/webjwt": "^1.0.9",
"@tsclass/tsclass": "^9.2.0" "@tsclass/tsclass": "^9.2.0"
}, },

874
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '5.0.6', version: '5.3.0',
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
} }

View File

@@ -16,14 +16,20 @@ 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 { CloudlyServerManager } from './manager.server/classes.servermanager.js'; import { CloudlyNodeManager } from './manager.node/classes.nodemanager.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';
/** /**
* Cloudly class can be used to instantiate a cloudly server. * Cloudly class can be used to instantiate a cloudly server.
@@ -52,13 +58,19 @@ export class Cloudly {
// managers // managers
public authManager: CloudlyAuthManager; public authManager: CloudlyAuthManager;
public secretManager: CloudlySecretManager; public secretManager: CloudlySecretManager;
public settingsManager: CloudlySettingsManager;
public clusterManager: ClusterManager; public clusterManager: ClusterManager;
public coreflowManager: CloudlyCoreflowManager; public coreflowManager: CloudlyCoreflowManager;
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 serverManager: CloudlyServerManager; public deploymentManager: DeploymentManager;
public dnsManager: DnsManager;
public domainManager: DomainManager;
public taskManager: CloudlyTaskManager;
public nodeManager: CloudlyNodeManager;
public baremetalManager: CloudlyBaremetalManager;
private readyDeferred = new plugins.smartpromise.Deferred(); private readyDeferred = new plugins.smartpromise.Deferred();
@@ -79,14 +91,20 @@ export class Cloudly {
// managers // managers
this.authManager = new CloudlyAuthManager(this); this.authManager = new CloudlyAuthManager(this);
this.settingsManager = new CloudlySettingsManager(this);
this.clusterManager = new ClusterManager(this); this.clusterManager = new ClusterManager(this);
this.coreflowManager = new CloudlyCoreflowManager(this); this.coreflowManager = new CloudlyCoreflowManager(this);
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.serverManager = new CloudlyServerManager(this); this.nodeManager = new CloudlyNodeManager(this);
this.baremetalManager = new CloudlyBaremetalManager(this);
} }
/** /**
@@ -97,13 +115,21 @@ export class Cloudly {
// config // config
await this.config.init(this.configOptions); await this.config.init(this.configOptions);
// database (data comes from config)
await this.mongodbConnector.init();
// settings (are stored in db)
await this.settingsManager.init();
// manageers // manageers
await this.authManager.start(); await this.authManager.start();
await this.secretManager.start(); await this.secretManager.start();
await this.serverManager.start(); await this.nodeManager.start();
await this.baremetalManager.start();
// connectors await this.serviceManager.start();
await this.mongodbConnector.init(); await this.deploymentManager.start();
await this.taskManager.init();
await this.cloudflareConnector.init(); await this.cloudflareConnector.init();
await this.letsencryptConnector.init(); await this.letsencryptConnector.init();
await this.clusterManager.init(); await this.clusterManager.init();
@@ -112,6 +138,7 @@ export class Cloudly {
// start the managers // start the managers
this.imageManager.start(); this.imageManager.start();
this.externalRegistryManager.start();
} }
/** /**
@@ -122,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

@@ -20,10 +20,8 @@ export class CloudlyConfig {
await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>( await plugins.npmextra.AppData.createAndInit<plugins.servezoneInterfaces.data.ICloudlyConfig>(
{ {
envMapping: { envMapping: {
cfToken: 'CF_TOKEN',
environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration', environment: 'SERVEZONE_ENVIRONMENT' as 'production' | 'integration',
letsEncryptEmail: 'hard:domains@lossless.org', letsEncryptEmail: 'hard:domains@lossless.org',
hetznerToken: 'HETZNER_API_TOKEN',
letsEncryptPrivateKey: null, letsEncryptPrivateKey: null,
publicUrl: 'SERVEZONE_URL', publicUrl: 'SERVEZONE_URL',
publicPort: 'SERVEZONE_PORT', publicPort: 'SERVEZONE_PORT',
@@ -46,8 +44,6 @@ export class CloudlyConfig {
servezoneAdminaccount: 'SERVEZONE_ADMINACCOUNT', servezoneAdminaccount: 'SERVEZONE_ADMINACCOUNT',
}, },
requiredKeys: [ requiredKeys: [
'cfToken',
'hetznerToken',
'letsEncryptEmail', 'letsEncryptEmail',
'publicUrl', 'publicUrl',
'publicPort', 'publicPort',

View File

@@ -95,7 +95,7 @@ export class CloudlyServer {
this.typedServer.typedrouter.addTypedRouter(this.typedrouter); this.typedServer.typedrouter.addTypedRouter(this.typedrouter);
this.typedServer.server.addRoute( this.typedServer.server.addRoute(
'/curlfresh/:scriptname', '/curlfresh/:scriptname',
this.cloudlyRef.serverManager.curlfreshInstance.handler, this.cloudlyRef.nodeManager.curlfreshInstance.handler,
); );
await this.typedServer.start(); await this.typedServer.start();
} }

View File

@@ -14,6 +14,13 @@ export class CloudflareConnector {
// init the instance // init the instance
public async init() { public async init() {
this.cloudflare = new plugins.cloudflare.CloudflareAccount(this.cloudlyRef.config.data.cfToken); const cloudflareToken = await this.cloudlyRef.settingsManager.getSetting('cloudflareToken');
if (!cloudflareToken) {
console.log('warn', 'No Cloudflare token configured in settings. Cloudflare features will be disabled.');
return;
}
this.cloudflare = new plugins.cloudflare.CloudflareAccount(cloudflareToken);
} }
} }

View File

@@ -0,0 +1,104 @@
import * as plugins from '../plugins.js';
/**
* BareMetal represents an actual physical server
*/
@plugins.smartdata.Manager()
export class BareMetal extends plugins.smartdata.SmartDataDbDoc<
BareMetal,
plugins.servezoneInterfaces.data.IBareMetal
> {
// STATIC
public static async createFromHetznerServer(
hetznerServerArg: plugins.hetznercloud.HetznerServer,
) {
const newBareMetal = new BareMetal();
newBareMetal.id = plugins.smartunique.shortId(8);
const data: plugins.servezoneInterfaces.data.IBareMetal['data'] = {
hostname: hetznerServerArg.data.name,
primaryIp: hetznerServerArg.data.public_net.ipv4.ip,
provider: 'hetzner',
location: hetznerServerArg.data.datacenter.name,
specs: {
cpuModel: hetznerServerArg.data.server_type.cpu_type,
cpuCores: hetznerServerArg.data.server_type.cores,
memoryGB: hetznerServerArg.data.server_type.memory,
storageGB: hetznerServerArg.data.server_type.disk,
storageType: 'nvme',
},
powerState: hetznerServerArg.data.status === 'running' ? 'on' : 'off',
osInfo: {
name: 'Debian',
version: '12',
},
assignedNodeIds: [],
providerMetadata: {
hetznerServerId: hetznerServerArg.data.id,
hetznerServerName: hetznerServerArg.data.name,
},
};
Object.assign(newBareMetal, { data });
await newBareMetal.save();
return newBareMetal;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IBareMetal['data'];
constructor() {
super();
}
public async assignNode(nodeId: string) {
if (!this.data.assignedNodeIds.includes(nodeId)) {
this.data.assignedNodeIds.push(nodeId);
await this.save();
}
}
public async removeNode(nodeId: string) {
this.data.assignedNodeIds = this.data.assignedNodeIds.filter(id => id !== nodeId);
await this.save();
}
public async updatePowerState(state: 'on' | 'off' | 'unknown') {
this.data.powerState = state;
await this.save();
}
public async powerOn(): Promise<boolean> {
// TODO: Implement IPMI power on
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
// Implement IPMI power on command
console.log(`Powering on BareMetal ${this.id} via IPMI`);
await this.updatePowerState('on');
return true;
}
return false;
}
public async powerOff(): Promise<boolean> {
// TODO: Implement IPMI power off
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
// Implement IPMI power off command
console.log(`Powering off BareMetal ${this.id} via IPMI`);
await this.updatePowerState('off');
return true;
}
return false;
}
public async reset(): Promise<boolean> {
// TODO: Implement IPMI reset
if (this.data.ipmiAddress && this.data.ipmiCredentials) {
// Implement IPMI reset command
console.log(`Resetting BareMetal ${this.id} via IPMI`);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,176 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { BareMetal } from './classes.baremetal.js';
import { logger } from '../logger.js';
export class CloudlyBaremetalManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CBareMetal = plugins.smartdata.setDefaultManagerForDoc(this, BareMetal);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
this.cloudlyRef.typedrouter.addTypedRouter(this.typedRouter);
// API endpoint to get baremetal servers
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_GetBaremetalServers>(
'getBaremetalServers',
async (requestData) => {
const baremetals = await this.getAllBaremetals();
return {
baremetals: await Promise.all(
baremetals.map((baremetal) => baremetal.createSavableObject())
),
};
},
),
);
// API endpoint to control baremetal via IPMI
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.baremetal.IRequest_Any_Cloudly_ControlBaremetal>(
'controlBaremetal',
async (requestData) => {
const baremetal = await this.CBareMetal.getInstance({
id: requestData.baremetalId,
});
if (!baremetal) {
return {
success: false,
message: 'BareMetal not found',
};
}
let success = false;
switch (requestData.action) {
case 'powerOn':
success = await baremetal.powerOn();
break;
case 'powerOff':
success = await baremetal.powerOff();
break;
case 'reset':
success = await baremetal.reset();
break;
}
return {
success,
message: success ? `Action ${requestData.action} completed` : `Action ${requestData.action} failed`,
};
},
),
);
}
public async start() {
const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken');
if (hetznerToken) {
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken);
}
logger.log('info', 'BareMetal manager started');
}
public async stop() {
logger.log('info', 'BareMetal manager stopped');
}
/**
* Get all baremetal servers
*/
public async getAllBaremetals(): Promise<BareMetal[]> {
const baremetals = await this.CBareMetal.getInstances({});
return baremetals;
}
/**
* Get baremetal by ID
*/
public async getBaremetalById(id: string): Promise<BareMetal | null> {
const baremetal = await this.CBareMetal.getInstance({
id,
});
return baremetal;
}
/**
* Get baremetals by provider
*/
public async getBaremetalsByProvider(provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise'): Promise<BareMetal[]> {
const baremetals = await this.CBareMetal.getInstances({
data: {
provider,
},
});
return baremetals;
}
/**
* Create baremetal from Hetzner server
*/
public async createBaremetalFromHetznerServer(hetznerServer: plugins.hetznercloud.HetznerServer): Promise<BareMetal> {
// Check if baremetal already exists for this Hetzner server
const existingBaremetals = await this.CBareMetal.getInstances({});
for (const baremetal of existingBaremetals) {
if (baremetal.data.providerMetadata?.hetznerServerId === hetznerServer.data.id) {
logger.log('info', `BareMetal already exists for Hetzner server ${hetznerServer.data.id}`);
return baremetal;
}
}
// Create new baremetal
const newBaremetal = await BareMetal.createFromHetznerServer(hetznerServer);
logger.log('success', `Created new BareMetal ${newBaremetal.id} from Hetzner server ${hetznerServer.data.id}`);
return newBaremetal;
}
/**
* Sync baremetals with Hetzner
*/
public async syncWithHetzner() {
if (!this.hetznerAccount) {
logger.log('warn', 'Cannot sync with Hetzner - no account configured');
return;
}
const hetznerServers = await this.hetznerAccount.getServers();
for (const hetznerServer of hetznerServers) {
await this.createBaremetalFromHetznerServer(hetznerServer);
}
logger.log('success', `Synced ${hetznerServers.length} servers from Hetzner`);
}
/**
* Provision a new baremetal server
*/
public async provisionBaremetal(options: {
provider: 'hetzner' | 'aws' | 'digitalocean';
location: any; // TODO: Import proper type from hetznercloud when available
type: any; // TODO: Import proper type from hetznercloud when available
}): Promise<BareMetal> {
if (options.provider === 'hetzner' && this.hetznerAccount) {
const hetznerServer = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('baremetal'),
location: options.location,
type: options.type,
});
const baremetal = await this.createBaremetalFromHetznerServer(hetznerServer);
return baremetal;
}
throw new Error(`Provider ${options.provider} not supported or not configured`);
}
}

View File

@@ -24,19 +24,26 @@ export class ClusterManager {
this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>( this.typedrouter.addTypedHandler<plugins.servezoneInterfaces.requests.cluster.IRequest_CreateCluster>(
new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => { new plugins.typedrequest.TypedHandler('createCluster', async (dataArg) => {
// TODO: guards // TODO: guards
const setupMode = dataArg.setupMode || 'manual'; // Default to manual if not specified
const cluster = await this.createCluster({ const cluster = await this.createCluster({
id: plugins.smartunique.uniSimple('cluster'), id: plugins.smartunique.uniSimple('cluster'),
data: { data: {
userId: null, // this is created by the createCluster method userId: null, // this is created by the createCluster method
name: dataArg.clusterName, name: dataArg.clusterName,
setupMode: setupMode,
acmeInfo: null, acmeInfo: null,
cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`, cloudlyUrl: `https://${this.cloudlyRef.config.data.publicUrl}:${this.cloudlyRef.config.data.publicPort}/`,
servers: [], nodes: [],
sshKeys: [], sshKeys: [],
}, },
}); });
console.log(await cluster.createSavableObject()); console.log(await cluster.createSavableObject());
this.cloudlyRef.serverManager.ensureServerInfrastructure();
// Only auto-provision servers if setupMode is 'hetzner'
if (setupMode === 'hetzner') {
this.cloudlyRef.nodeManager.ensureNodeInfrastructure();
}
return { return {
cluster: await cluster.createSavableObject(), cluster: await cluster.createSavableObject(),
}; };

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;
}
// Add typedrouter to cloudly's main router
public async start() { this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter);
// lets set up a 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

@@ -0,0 +1,61 @@
import * as plugins from '../plugins.js';
/**
* ClusterNode represents a logical node participating in a cluster
*/
@plugins.smartdata.Manager()
export class ClusterNode extends plugins.smartdata.SmartDataDbDoc<
ClusterNode,
plugins.servezoneInterfaces.data.IClusterNode
> {
// STATIC
public static async createFromHetznerServer(
hetznerServerArg: plugins.hetznercloud.HetznerServer,
clusterId: string,
baremetalId: string,
) {
const newNode = new ClusterNode();
newNode.id = plugins.smartunique.shortId(8);
const data: plugins.servezoneInterfaces.data.IClusterNode['data'] = {
clusterId: clusterId,
baremetalId: baremetalId,
nodeType: 'baremetal',
status: 'initializing',
role: 'worker',
joinedAt: Date.now(),
lastHealthCheck: Date.now(),
sshKeys: [],
requiredDebianPackages: [],
};
Object.assign(newNode, { data });
await newNode.save();
return newNode;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IClusterNode['data'];
constructor() {
super();
}
public async getDeployments(): Promise<plugins.servezoneInterfaces.data.IDeployment[]> {
// TODO: Implement getting deployments for this node
return [];
}
public async updateMetrics(metrics: plugins.servezoneInterfaces.data.IClusterNodeMetrics) {
this.data.metrics = metrics;
this.data.lastHealthCheck = Date.now();
await this.save();
}
public async updateStatus(status: plugins.servezoneInterfaces.data.IClusterNode['data']['status']) {
this.data.status = status;
await this.save();
}
}

View File

@@ -1,6 +1,6 @@
import { logger } from '../logger.js'; import { logger } from '../logger.js';
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import type { CloudlyServerManager } from './classes.servermanager.js'; import type { CloudlyNodeManager } from './classes.nodemanager.js';
export class CurlFresh { export class CurlFresh {
public optionsArg = { public optionsArg = {
@@ -45,7 +45,7 @@ bash -c "spark installdaemon"
`, `,
}; };
public serverManagerRef: CloudlyServerManager; public nodeManagerRef: CloudlyNodeManager;
public curlFreshRoute: plugins.typedserver.servertools.Route; public curlFreshRoute: plugins.typedserver.servertools.Route;
public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => { public handler = new plugins.typedserver.servertools.Handler('ALL', async (req, res) => {
logger.log('info', 'curlfresh handler called. a server might be coming online soon :)'); logger.log('info', 'curlfresh handler called. a server might be coming online soon :)');
@@ -62,12 +62,12 @@ bash -c "spark installdaemon"
} }
}); });
constructor(serverManagerRefArg: CloudlyServerManager) { constructor(nodeManagerRefArg: CloudlyNodeManager) {
this.serverManagerRef = serverManagerRefArg; this.nodeManagerRef = nodeManagerRefArg;
} }
public async getServerUserData(): Promise<string> { public async getServerUserData(): Promise<string> {
const sslMode = const sslMode =
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode'); await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('sslMode');
let protocol: 'http' | 'https'; let protocol: 'http' | 'https';
if (sslMode === 'none') { if (sslMode === 'none') {
protocol = 'http'; protocol = 'http';
@@ -76,9 +76,9 @@ bash -c "spark installdaemon"
} }
const domain = const domain =
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl'); await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicUrl');
const port = const port =
await this.serverManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort'); await this.nodeManagerRef.cloudlyRef.config.appData.waitForAndGetKey('publicPort');
const serverUserData = `#cloud-config const serverUserData = `#cloud-config
runcmd: runcmd:

View File

@@ -0,0 +1,131 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { Cluster } from '../manager.cluster/classes.cluster.js';
import { ClusterNode } from './classes.clusternode.js';
import { CurlFresh } from './classes.curlfresh.js';
export class CloudlyNodeManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
public curlfreshInstance = new CurlFresh(this);
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CClusterNode = plugins.smartdata.setDefaultManagerForDoc(this, ClusterNode);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
/**
* is used be serverconfig module on the node to get the actual node config
*/
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetNodeConfig>(
'getNodeConfig',
async (requestData) => {
const nodeId = requestData.nodeId;
const node = await this.CClusterNode.getInstance({
id: nodeId,
});
return {
configData: await node.createSavableObject(),
};
},
),
);
}
public async start() {
const hetznerToken = await this.cloudlyRef.settingsManager.getSetting('hetznerToken');
if (!hetznerToken) {
console.log('warn', 'No Hetzner token configured in settings. Hetzner features will be disabled.');
return;
}
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(hetznerToken);
}
public async stop() {}
/**
* creates the node infrastructure on hetzner
* ensures that there are exactly the resources that are needed
* no more, no less
*/
public async ensureNodeInfrastructure() {
// get all clusters
const allClusters = await this.cloudlyRef.clusterManager.getAllClusters();
for (const cluster of allClusters) {
// Skip clusters that are not set up for Hetzner auto-provisioning
if (cluster.data.setupMode !== 'hetzner') {
console.log(`Skipping node provisioning for cluster ${cluster.id} - setupMode is ${cluster.data.setupMode || 'manual'}`);
continue;
}
// get existing nodes
const nodes = await this.getNodesByCluster(cluster);
// if there is no node, create one
if (nodes.length === 0) {
const hetznerServer = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('node'),
location: 'nbg1',
type: 'cpx41',
labels: {
clusterId: cluster.id,
priority: '1',
},
userData: await this.curlfreshInstance.getServerUserData(),
});
// First create BareMetal record
const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer);
const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id);
await baremetal.assignNode(newNode.id);
console.log(`cluster created new node for cluster ${cluster.id}`);
} else {
console.log(
`cluster ${cluster.id} already has nodes. Making sure that they actually exist in the real world...`,
);
// if there is a node, make sure that it exists
for (const node of nodes) {
const hetznerServers = await this.hetznerAccount.getServersByLabel({
clusterId: cluster.id,
});
if (!hetznerServers || hetznerServers.length === 0) {
console.log(`node ${node.id} does not exist in the real world. Creating it now...`);
const hetznerServer = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('node'),
location: 'nbg1',
type: 'cpx41',
labels: {
clusterId: cluster.id,
priority: '1',
},
});
// First create BareMetal record
const baremetal = await this.cloudlyRef.baremetalManager.createBaremetalFromHetznerServer(hetznerServer);
const newNode = await ClusterNode.createFromHetznerServer(hetznerServer, cluster.id, baremetal.id);
await baremetal.assignNode(newNode.id);
}
}
}
}
}
public async getNodesByCluster(clusterArg: Cluster) {
const results = await this.CClusterNode.getInstances({
data: {
clusterId: clusterArg.id,
},
});
return results;
}
}

View File

@@ -1,42 +0,0 @@
import * as plugins from '../plugins.js';
/*
* cluster defines a swarmkit cluster
*/
@plugins.smartdata.Manager()
export class Server extends plugins.smartdata.SmartDataDbDoc<
Server,
plugins.servezoneInterfaces.data.IServer
> {
// STATIC
public static async createFromHetznerServer(
hetznerServerArg: plugins.hetznercloud.HetznerServer,
) {
const newServer = new Server();
newServer.id = plugins.smartunique.shortId(8);
const data: plugins.servezoneInterfaces.data.IServer['data'] = {
assignedClusterId: hetznerServerArg.data.labels.clusterId,
requiredDebianPackages: [],
sshKeys: [],
type: 'hetzner',
};
Object.assign(newServer, { data });
await newServer.save();
return newServer;
}
// INSTANCE
@plugins.smartdata.unI()
public id: string;
@plugins.smartdata.svDb()
public data: plugins.servezoneInterfaces.data.IServer['data'];
constructor() {
super();
}
public async getServices(): Promise<plugins.servezoneInterfaces.data.IService[]> {
return [];
}
}

View File

@@ -1,110 +0,0 @@
import * as plugins from '../plugins.js';
import { Cloudly } from '../classes.cloudly.js';
import { Cluster } from '../manager.cluster/classes.cluster.js';
import { Server } from './classes.server.js';
import { CurlFresh } from './classes.curlfresh.js';
export class CloudlyServerManager {
public cloudlyRef: Cloudly;
public typedRouter = new plugins.typedrequest.TypedRouter();
public curlfreshInstance = new CurlFresh(this);
public hetznerAccount: plugins.hetznercloud.HetznerAccount;
public get db() {
return this.cloudlyRef.mongodbConnector.smartdataDb;
}
public CServer = plugins.smartdata.setDefaultManagerForDoc(this, Server);
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
/**
* is used be serverconfig module on the server to get the actual server config
*/
this.typedRouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.config.IRequest_Any_Cloudly_GetServerConfig>(
'getServerConfig',
async (requestData) => {
const serverId = requestData.serverId;
const server = await this.CServer.getInstance({
id: serverId,
});
return {
configData: await server.createSavableObject(),
};
},
),
);
}
public async start() {
this.hetznerAccount = new plugins.hetznercloud.HetznerAccount(
this.cloudlyRef.config.data.hetznerToken,
);
}
public async stop() {}
/**
* creates the server infrastructure on hetzner
* ensures that there are exactly the reources that are needed
* no more, no less
*/
public async ensureServerInfrastructure() {
// get all clusters
const allClusters = await this.cloudlyRef.clusterManager.getAllClusters();
for (const cluster of allClusters) {
// get existing servers
const servers = await this.getServersByCluster(cluster);
// if there is no server, create one
if (servers.length === 0) {
const server = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('server'),
location: 'nbg1',
type: 'cpx41',
labels: {
clusterId: cluster.id,
priority: '1',
},
userData: await this.curlfreshInstance.getServerUserData(),
});
const newServer = await Server.createFromHetznerServer(server);
console.log(`cluster created new server for cluster ${cluster.id}`);
} else {
console.log(
`cluster ${cluster.id} already has servers. Making sure that they actually exist in the real world...`,
);
// if there is a server, make sure that it exists
for (const server of servers) {
const hetznerServer = await this.hetznerAccount.getServersByLabel({
clusterId: cluster.id,
});
if (!hetznerServer) {
console.log(`server ${server.id} does not exist in the real world. Creating it now...`);
const hetznerServer = await this.hetznerAccount.createServer({
name: plugins.smartunique.uniSimple('server'),
location: 'nbg1',
type: 'cpx41',
labels: {
clusterId: cluster.id,
priority: '1',
},
});
const newServer = await Server.createFromHetznerServer(hetznerServer);
}
}
}
}
}
public async getServersByCluster(clusterArg: Cluster) {
const results = await this.CServer.getInstances({
data: {
assignedClusterId: clusterArg.id,
},
});
return results;
}
}

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

@@ -14,6 +14,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>(
@@ -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,255 @@
import * as plugins from '../plugins.js';
import type { Cloudly } from '../classes.cloudly.js';
import * as servezoneInterfaces from '@serve.zone/interfaces';
export class CloudlySettingsManager {
public cloudlyRef: Cloudly;
public readyDeferred = plugins.smartpromise.defer();
public settingsStore: plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
constructor(cloudlyRefArg: Cloudly) {
this.cloudlyRef = cloudlyRefArg;
}
/**
* Initialize the settings manager and create the EasyStore
*/
public async init() {
this.settingsStore = await this.cloudlyRef.mongodbConnector.smartdataDb
.createEasyStore('cloudly-settings') as plugins.smartdata.EasyStore<servezoneInterfaces.data.ICloudlySettings>;
// Setup API route handlers
await this.setupRoutes();
this.readyDeferred.resolve();
}
/**
* Get all settings
*/
public async getSettings(): Promise<servezoneInterfaces.data.ICloudlySettings> {
await this.readyDeferred.promise;
return await this.settingsStore.readAll();
}
/**
* Get all settings with masked sensitive values (for API responses)
*/
public async getSettingsMasked(): Promise<servezoneInterfaces.data.ICloudlySettingsMasked> {
await this.readyDeferred.promise;
const settings = await this.getSettings();
const masked: servezoneInterfaces.data.ICloudlySettingsMasked = {};
for (const [key, value] of Object.entries(settings)) {
if (typeof value === 'string' && value.length > 4) {
// Mask the token, showing only last 4 characters
masked[key] = '****' + value.slice(-4);
} else {
masked[key] = value;
}
}
return masked;
}
/**
* Update multiple settings at once
*/
public async updateSettings(updates: Partial<servezoneInterfaces.data.ICloudlySettings>): Promise<void> {
await this.readyDeferred.promise;
for (const [key, value] of Object.entries(updates)) {
if (value !== undefined && value !== '') {
await this.settingsStore.writeKey(key as keyof servezoneInterfaces.data.ICloudlySettings, value);
} else if (value === '') {
// Empty string means clear the setting
await this.settingsStore.deleteKey(key as keyof servezoneInterfaces.data.ICloudlySettings);
}
}
}
/**
* Get a specific setting value
*/
public async getSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K): Promise<servezoneInterfaces.data.ICloudlySettings[K]> {
await this.readyDeferred.promise;
return await this.settingsStore.readKey(key);
}
/**
* Set a specific setting value
*/
public async setSetting<K extends keyof servezoneInterfaces.data.ICloudlySettings>(key: K, value: servezoneInterfaces.data.ICloudlySettings[K]): Promise<void> {
await this.readyDeferred.promise;
if (value !== undefined && value !== '') {
await this.settingsStore.writeKey(key, value);
}
}
/**
* Clear a specific setting
*/
public async clearSetting(key: keyof servezoneInterfaces.data.ICloudlySettings): Promise<void> {
await this.readyDeferred.promise;
await this.settingsStore.deleteKey(key);
}
/**
* Clear all settings
*/
public async clearAllSettings(): Promise<void> {
await this.readyDeferred.promise;
await this.settingsStore.wipe();
}
/**
* Test connection for a specific provider
*/
public async testProviderConnection(provider: string): Promise<{success: boolean; message: string}> {
await this.readyDeferred.promise;
try {
switch (provider) {
case 'hetzner':
const hetznerToken = await this.getSetting('hetznerToken');
if (!hetznerToken) {
return { success: false, message: 'No Hetzner token configured' };
}
// TODO: Implement actual Hetzner API test
return { success: true, message: 'Hetzner connection test successful' };
case 'cloudflare':
const cloudflareToken = await this.getSetting('cloudflareToken');
if (!cloudflareToken) {
return { success: false, message: 'No Cloudflare token configured' };
}
// TODO: Implement actual Cloudflare API test
return { success: true, message: 'Cloudflare connection test successful' };
case 'aws':
const awsKey = await this.getSetting('awsAccessKey');
const awsSecret = await this.getSetting('awsSecretKey');
if (!awsKey || !awsSecret) {
return { success: false, message: 'AWS credentials not configured' };
}
// TODO: Implement actual AWS API test
return { success: true, message: 'AWS connection test successful' };
case 'digitalocean':
const doToken = await this.getSetting('digitalOceanToken');
if (!doToken) {
return { success: false, message: 'No DigitalOcean token configured' };
}
// TODO: Implement actual DigitalOcean API test
return { success: true, message: 'DigitalOcean connection test successful' };
case 'azure':
const azureClientId = await this.getSetting('azureClientId');
const azureClientSecret = await this.getSetting('azureClientSecret');
const azureTenantId = await this.getSetting('azureTenantId');
if (!azureClientId || !azureClientSecret || !azureTenantId) {
return { success: false, message: 'Azure credentials not configured' };
}
// TODO: Implement actual Azure API test
return { success: true, message: 'Azure connection test successful' };
default:
return { success: false, message: `Unknown provider: ${provider}` };
}
} catch (error) {
return { success: false, message: `Connection test failed: ${error.message}` };
}
}
/**
* Setup API route handlers for settings management
*/
private async setupRoutes() {
// Get Settings Handler
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSettings>(
'getSettings',
async (requestData) => {
// TODO: Add authentication check for admin users
const maskedSettings = await this.getSettingsMasked();
return {
settings: maskedSettings
};
}
)
);
// Update Settings Handler
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_UpdateSettings>(
'updateSettings',
async (requestData) => {
// TODO: Add authentication check for admin users
try {
await this.updateSettings(requestData.updates);
return {
success: true,
message: 'Settings updated successfully'
};
} catch (error) {
return {
success: false,
message: `Failed to update settings: ${error.message}`
};
}
}
)
);
// Clear Setting Handler
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_ClearSetting>(
'clearSetting',
async (requestData) => {
// TODO: Add authentication check for admin users
try {
await this.clearSetting(requestData.key);
return {
success: true,
message: `Setting ${requestData.key} cleared successfully`
};
} catch (error) {
return {
success: false,
message: `Failed to clear setting: ${error.message}`
};
}
}
)
);
// Test Provider Connection Handler
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_TestProviderConnection>(
'testProviderConnection',
async (requestData) => {
// TODO: Add authentication check for admin users
const testResult = await this.testProviderConnection(requestData.provider);
return {
success: testResult.success,
message: testResult.message,
connectionValid: testResult.success
};
}
)
);
// Get Single Setting Handler (for internal use)
this.cloudlyRef.typedrouter.addTypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
new plugins.typedrequest.TypedHandler<servezoneInterfaces.requests.settings.IRequest_GetSetting>(
'getSetting',
async (requestData) => {
// TODO: Add authentication check for admin users
const value = await this.getSetting(requestData.key);
return {
value
};
}
)
);
}
}

View File

@@ -0,0 +1 @@
export * from './classes.settingsmanager.js';

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
@@ -32,4 +34,4 @@ import * as tsclass from '@tsclass/tsclass';
export { export {
tsclass, tsclass,
} }

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();
};

View File

@@ -0,0 +1,73 @@
import * as plugins from '../plugins.js';
export interface IBareMetal {
id: string;
data: {
hostname: string;
/**
* IPMI management IP address
*/
ipmiAddress?: string;
/**
* Encrypted IPMI credentials
*/
ipmiCredentials?: {
username: string;
passwordEncrypted: string;
};
/**
* Primary network IP address
*/
primaryIp: string;
/**
* Provider of the physical server
*/
provider: 'hetzner' | 'aws' | 'digitalocean' | 'onpremise';
/**
* Data center or location
*/
location: string;
/**
* Hardware specifications
*/
specs: {
cpuModel: string;
cpuCores: number;
memoryGB: number;
storageGB: number;
storageType: 'ssd' | 'hdd' | 'nvme';
};
/**
* Current power state
*/
powerState: 'on' | 'off' | 'unknown';
/**
* Operating system information
*/
osInfo: {
name: string;
version: string;
kernel?: string;
};
/**
* Array of ClusterNode IDs running on this hardware
*/
assignedNodeIds: string[];
/**
* Metadata for provider-specific information
*/
providerMetadata?: {
[key: string]: any;
};
};
}

View File

@@ -1,8 +1,6 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
export interface ICloudlyConfig { export interface ICloudlyConfig {
cfToken?: string;
hetznerToken?: string;
environment?: 'production' | 'integration'; environment?: 'production' | 'integration';
letsEncryptEmail?: string; letsEncryptEmail?: string;
letsEncryptPrivateKey?: string; letsEncryptPrivateKey?: string;

View File

@@ -1,7 +1,7 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import { type IDockerRegistryInfo } from '../data/docker.js'; import { type IDockerRegistryInfo } from '../data/docker.js';
import type { IServer } from './server.js'; import type { IClusterNode } from './clusternode.js';
export interface ICluster { export interface ICluster {
id: string; id: string;
@@ -19,9 +19,14 @@ export interface ICluster {
cloudlyUrl?: string; cloudlyUrl?: string;
/** /**
* what servers are expected to be part of the cluster * Cluster setup mode - manual by default, or auto-provision with cloud provider
*/ */
servers: IServer[]; setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
/**
* Nodes that are part of the cluster
*/
nodes: IClusterNode[];
/** /**
* ACME info. This is used to get SSL certificates. * ACME info. This is used to get SSL certificates.

View File

@@ -0,0 +1,71 @@
import * as plugins from '../plugins.js';
export interface IClusterNodeMetrics {
cpuUsagePercent: number;
memoryUsedMB: number;
memoryAvailableMB: number;
diskUsedGB: number;
diskAvailableGB: number;
containerCount: number;
timestamp: number;
}
export interface IClusterNode {
id: string;
data: {
/**
* Reference to the cluster this node belongs to
*/
clusterId: string;
/**
* Reference to the physical server (if applicable)
*/
baremetalId?: string;
/**
* Type of node
*/
nodeType: 'baremetal' | 'vm' | 'container';
/**
* Current status of the node
*/
status: 'initializing' | 'online' | 'offline' | 'maintenance';
/**
* Role of the node in the cluster
*/
role: 'master' | 'worker';
/**
* Timestamp when node joined the cluster
*/
joinedAt: number;
/**
* Last health check timestamp
*/
lastHealthCheck: number;
/**
* Current metrics for the node
*/
metrics?: IClusterNodeMetrics;
/**
* Docker swarm node ID if part of swarm
*/
swarmNodeId?: string;
/**
* SSH keys deployed to this node
*/
sshKeys: plugins.tsclass.network.ISshKey[];
/**
* Debian packages installed on this node
*/
requiredDebianPackages: string[];
};
}

View File

@@ -6,8 +6,58 @@ import * as plugins from '../plugins.js';
*/ */
export interface IDeployment { export interface IDeployment {
id: string; id: string;
affectedServiceIds: string[];
/**
* The service being deployed (single service per deployment)
*/
serviceId: string;
/**
* The node this deployment is running on
*/
nodeId: string;
/**
* Docker container ID for this deployment
*/
containerId?: string;
/**
* Image used for this deployment
*/
usedImageId: string; usedImageId: string;
/**
* Version of the service deployed
*/
version: string;
/**
* Timestamp when deployed
*/
deployedAt: number;
/**
* Deployment log entries
*/
deploymentLog: string[]; deploymentLog: string[];
status: 'scheduled' | 'running' | 'deployed' | 'failed';
/**
* Current status of the deployment
*/
status: 'scheduled' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed';
/**
* Health status of the deployment
*/
healthStatus?: 'healthy' | 'unhealthy' | 'unknown';
/**
* Resource usage for this deployment
*/
resourceUsage?: {
cpuUsagePercent: number;
memoryUsedMB: number;
lastUpdated: number;
};
} }

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,15 +2,20 @@ 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';
export * from './secretbundle.js'; export * from './secretbundle.js';
export * from './secretgroup.js' export * from './secretgroup.js';
export * from './server.js'; export * from './baremetal.js';
export * from './clusternode.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

@@ -17,6 +17,35 @@ export interface IService {
* and thus live past the service lifecycle * and thus live past the service lifecycle
*/ */
additionalSecretBundleIds?: string[]; additionalSecretBundleIds?: string[];
/**
* Service category determines deployment behavior
* - base: Core services that run on every node (coreflow, coretraffic, corelog)
* - distributed: Services that run on limited nodes (cores3, coremongo)
* - workload: User applications
*/
serviceCategory: 'base' | 'distributed' | 'workload';
/**
* Deployment strategy for the service
* - all-nodes: Deploy to every node in the cluster
* - limited-replicas: Deploy to a limited number of nodes
* - custom: Custom deployment logic
*/
deploymentStrategy: 'all-nodes' | 'limited-replicas' | 'custom';
/**
* Maximum number of replicas for distributed services
* For example, 3 for cores3 or coremongo
*/
maxReplicas?: number;
/**
* Whether to enforce anti-affinity rules
* When true, tries to spread deployments across different BareMetal servers
*/
antiAffinity?: boolean;
scaleFactor: number; scaleFactor: number;
balancingStrategy: 'round-robin' | 'least-connections'; balancingStrategy: 'round-robin' | 'least-connections';
ports: { ports: {
@@ -25,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,56 @@
import * as plugins from '../plugins.js';
/**
* Interface for Cloudly settings stored in EasyStore
* These are runtime-configurable settings that can be modified via the UI
*/
export interface ICloudlySettings {
// Cloud Provider Tokens
hetznerToken?: string;
cloudflareToken?: string;
// AWS Credentials
awsAccessKey?: string;
awsSecretKey?: string;
awsRegion?: string;
// DigitalOcean
digitalOceanToken?: string;
// Azure Credentials
azureClientId?: string;
azureClientSecret?: string;
azureTenantId?: string;
azureSubscriptionId?: string;
// Google Cloud
googleCloudKeyJson?: string;
googleCloudProjectId?: string;
// Vultr
vultrApiKey?: string;
// Linode
linodeToken?: string;
// OVH
ovhApplicationKey?: string;
ovhApplicationSecret?: string;
ovhConsumerKey?: string;
// Scaleway
scalewayAccessKey?: string;
scalewaySecretKey?: string;
scalewayOrganizationId?: string;
// Other settings that might be added in the future
[key: string]: string | undefined;
}
/**
* Interface for masked settings (used in API responses)
* Shows only last 4 characters of sensitive tokens
*/
export type ICloudlySettingsMasked = {
[K in keyof ICloudlySettings]: string | undefined;
};

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,22 @@
import * as plugins from '../plugins.js';
import type { IBareMetal } from '../data/baremetal.js';
export interface IRequest_Any_Cloudly_GetBaremetalServers {
method: 'getBaremetalServers';
request: {};
response: {
baremetals: IBareMetal[];
};
}
export interface IRequest_Any_Cloudly_ControlBaremetal {
method: 'controlBaremetal';
request: {
baremetalId: string;
action: 'powerOn' | 'powerOff' | 'reset';
};
response: {
success: boolean;
message: string;
};
}

View File

@@ -41,6 +41,7 @@ export interface IRequest_CreateCluster extends plugins.typedrequestInterfaces.i
request: { request: {
identity: userInterfaces.IIdentity; identity: userInterfaces.IIdentity;
clusterName: string; clusterName: string;
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
}; };
response: { response: {
cluster: clusterInterfaces.ICluster; cluster: clusterInterfaces.ICluster;

View File

@@ -0,0 +1,141 @@
import * as plugins from '../plugins.js';
import type { IDeployment } from '../data/deployment.js';
import type { IIdentity } from '../data/user.js';
export interface IReq_Any_Cloudly_GetDeploymentById
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_GetDeploymentById
> {
method: 'getDeploymentById';
request: {
identity: IIdentity;
deploymentId: string;
};
response: {
deployment: IDeployment;
};
}
export interface IReq_Any_Cloudly_GetDeployments
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_GetDeployments
> {
method: 'getDeployments';
request: {
identity: IIdentity;
};
response: {
deployments: IDeployment[];
};
}
export interface IReq_Any_Cloudly_GetDeploymentsByService
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_GetDeploymentsByService
> {
method: 'getDeploymentsByService';
request: {
identity: IIdentity;
serviceId: string;
};
response: {
deployments: IDeployment[];
};
}
export interface IReq_Any_Cloudly_GetDeploymentsByNode
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_GetDeploymentsByNode
> {
method: 'getDeploymentsByNode';
request: {
identity: IIdentity;
nodeId: string;
};
response: {
deployments: IDeployment[];
};
}
export interface IReq_Any_Cloudly_CreateDeployment
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_CreateDeployment
> {
method: 'createDeployment';
request: {
identity: IIdentity;
deploymentData: Partial<IDeployment>;
};
response: {
deployment: IDeployment;
};
}
export interface IReq_Any_Cloudly_UpdateDeployment
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_UpdateDeployment
> {
method: 'updateDeployment';
request: {
identity: IIdentity;
deploymentId: string;
deploymentData: Partial<IDeployment>;
};
response: {
deployment: IDeployment;
};
}
export interface IReq_Any_Cloudly_DeleteDeploymentById
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_DeleteDeploymentById
> {
method: 'deleteDeploymentById';
request: {
identity: IIdentity;
deploymentId: string;
};
response: {
success: boolean;
};
}
export interface IReq_Any_Cloudly_RestartDeployment
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_RestartDeployment
> {
method: 'restartDeployment';
request: {
identity: IIdentity;
deploymentId: string;
};
response: {
success: boolean;
deployment: IDeployment;
};
}
export interface IReq_Any_Cloudly_ScaleDeployment
extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_Any_Cloudly_ScaleDeployment
> {
method: 'scaleDeployment';
request: {
identity: IIdentity;
deploymentId: string;
replicas: number;
};
response: {
success: boolean;
deployment: IDeployment;
};
}

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: {
@@ -69,4 +70,20 @@ export interface IReq_DeleteRegistryById extends plugins.typedrequestInterfaces.
response: { response: {
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

@@ -1,40 +1,54 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as adminRequests from './admin.js'; import * as adminRequests from './admin.js';
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';
import * as informRequests from './inform.js'; import * as informRequests from './inform.js';
import * as logRequests from './log.js'; import * as logRequests from './log.js';
import * as networkRequests from './network.js'; import * as networkRequests from './network.js';
import * as nodeRequests from './node.js';
import * as routingRequests from './routing.js'; import * as routingRequests from './routing.js';
import * as secretBundleRequests from './secretbundle.js'; import * as secretBundleRequests from './secretbundle.js';
import * as secretGroupRequests from './secretgroup.js'; import * as secretGroupRequests from './secretgroup.js';
import * as serverRequests from './server.js'; 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 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 {
adminRequests as admin, adminRequests as admin,
baremetalRequests as baremetal,
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,
informRequests as inform, informRequests as inform,
logRequests as log, logRequests as log,
networkRequests as network, networkRequests as network,
nodeRequests as node,
routingRequests as routing, routingRequests as routing,
secretBundleRequests as secretbundle, secretBundleRequests as secretbundle,
secretGroupRequests as secretgroup, secretGroupRequests as secretgroup,
serverRequests as server, serverRequests as server,
serviceRequests as service, serviceRequests as service,
settingsRequests as settings,
statusRequests as status, statusRequests as status,
taskRequests as task,
versionRequests as version, versionRequests as version,
}; };

View File

@@ -0,0 +1,33 @@
import * as plugins from '../plugins.js';
import type { IClusterNode } from '../data/clusternode.js';
import type { IDeployment } from '../data/deployment.js';
export interface IRequest_Any_Cloudly_GetNodeConfig {
method: 'getNodeConfig';
request: {
nodeId: string;
};
response: {
configData: IClusterNode;
};
}
export interface IRequest_Any_Cloudly_GetNodesByCluster {
method: 'getNodesByCluster';
request: {
clusterId: string;
};
response: {
nodes: IClusterNode[];
};
}
export interface IRequest_Any_Cloudly_GetNodeDeployments {
method: 'getNodeDeployments';
request: {
nodeId: string;
};
response: {
deployments: IDeployment[];
};
}

View File

@@ -0,0 +1,59 @@
import * as plugins from '../plugins.js';
import type { ICloudlySettings, ICloudlySettingsMasked } from '../data/settings.js';
// Get Settings
export interface IRequest_GetSettings extends plugins.typedrequestInterfaces.ITypedRequest {
method: 'getSettings';
request: {};
response: {
settings: ICloudlySettingsMasked;
};
}
// Update Settings
export interface IRequest_UpdateSettings extends plugins.typedrequestInterfaces.ITypedRequest {
method: 'updateSettings';
request: {
updates: Partial<ICloudlySettings>;
};
response: {
success: boolean;
message: string;
};
}
// Clear Specific Setting
export interface IRequest_ClearSetting extends plugins.typedrequestInterfaces.ITypedRequest {
method: 'clearSetting';
request: {
key: keyof ICloudlySettings;
};
response: {
success: boolean;
message: string;
};
}
// Test Provider Connection
export interface IRequest_TestProviderConnection extends plugins.typedrequestInterfaces.ITypedRequest {
method: 'testProviderConnection';
request: {
provider: 'hetzner' | 'cloudflare' | 'aws' | 'digitalocean' | 'azure' | 'google' | 'vultr' | 'linode' | 'ovh' | 'scaleway';
};
response: {
success: boolean;
message: string;
connectionValid: boolean;
};
}
// Get Single Setting (for internal use, not exposed to frontend)
export interface IRequest_GetSetting extends plugins.typedrequestInterfaces.ITypedRequest {
method: 'getSetting';
request: {
key: keyof ICloudlySettings;
};
response: {
value: string | undefined;
};
}

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

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/cloudly', name: '@serve.zone/cloudly',
version: '5.0.6', version: '5.3.0',
description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.' description: 'A comprehensive tool for managing containerized applications across multiple cloud providers using Docker Swarmkit, featuring web, CLI, and API interfaces.'
} }

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?: any[]; services?: plugins.interfaces.data.IService[];
deployments?: any[]; 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,83 +87,204 @@ 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
try {
apiClient.identity = loginStatePart.getState().identity;
const services = await apiClient.services.getServices();
currentState = {
...currentState,
services: services,
};
} catch (error) {
console.error('Failed to fetch services:', error);
currentState = {
...currentState,
services: [],
};
}
// Deployments
try {
apiClient.identity = loginStatePart.getState().identity;
const responseDeployments = await apiClient.deployments.getDeployments();
currentState = {
...currentState,
deployments: responseDeployments?.deployments || [],
};
} catch (error) {
console.error('Failed to fetch deployments:', error);
currentState = {
...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;
}); });
// Service Actions
export const createServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceData: plugins.interfaces.data.IService['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.services.createService(payloadArg.serviceData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const updateServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceId: string; serviceData: plugins.interfaces.data.IService['data'] }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.services.updateService(payloadArg.serviceId, payloadArg.serviceData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const deleteServiceAction = dataState.createAction(
async (statePartArg, payloadArg: { serviceId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.services.deleteService(payloadArg.serviceId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
// SecretGroup Actions // SecretGroup Actions
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;
} }
@@ -161,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;
} }
); );
@@ -179,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;
} }
); );
@@ -197,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;
} }
); );
@@ -220,49 +333,249 @@ 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;
} }
); );
// Deployment Actions
export const createDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.deployments.createDeployment(payloadArg.deploymentData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const updateDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentId: string; deploymentData: Partial<plugins.interfaces.data.IDeployment> }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.deployments.updateDeployment(payloadArg.deploymentId, payloadArg.deploymentData);
currentState = await dataState.dispatchAction(getAllDataAction, null);
return currentState;
}
);
export const deleteDeploymentAction = dataState.createAction(
async (statePartArg, payloadArg: { deploymentId: string }) => {
let currentState = statePartArg.getState();
apiClient.identity = loginStatePart.getState().identity;
await apiClient.deployments.deleteDeployment(payloadArg.deploymentId);
currentState = await dataState.dispatchAction(getAllDataAction, null);
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 (
statePartArg, statePartArg,
payloadArg: { payloadArg: {
clusterName: string; clusterName: string;
setupMode?: 'manual' | 'hetzner' | 'aws' | 'digitalocean';
} }
) => { ) => {
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,20 +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 { 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 {
@@ -41,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}`;
@@ -73,72 +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',
element: CloudlyViewOverview,
},
{
name: 'SecretGroups',
element: CloudlyViewSecretGroups,
},
{
name: 'SecretBundles',
element: CloudlyViewSecretBundles,
},
{
name: 'Clusters',
element: CloudlyViewClusters,
},
{
name: 'ExternalRegistries',
element: CloudlyViewExternalRegistries,
},
{
name: 'Images',
element: CloudlyViewImages,
},
{
name: 'Services',
element: CloudlyViewServices,
},
{
name: 'Testing & Building',
element: CloudlyViewServices,
},
{
name: 'Deployments',
element: CloudlyViewDeployments,
},
{
name: 'DNS',
element: CloudlyViewDns,
},
{
name: 'Mails',
element: CloudlyViewMails,
},
{
name: 'Logs',
element: CloudlyViewLogs,
},
{
name: 's3',
element: CloudlyViewS3,
},
{
name: 'DBs',
element: CloudlyViewDbs,
},
{
name: 'Backups',
element: CloudlyViewBackups,
},
{
name: 'Fleet',
element: CloudlyViewBackups,
}
] as plugins.deesCatalog.IView[]}
></dees-simple-appdash> ></dees-simple-appdash>
</dees-simple-login> </dees-simple-login>
</div> </div>
@@ -180,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,137 +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-form>
`,
menuOptions: [
{
name: 'create',
action: async (modalArg) => {
const data: {
clusterName: string;
} = (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,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-deployments')
export class CloudlyViewDeployments 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>Deployments</cloudly-sectionheading>
<dees-table
.heading1=${'Deployments'}
.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-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,67 +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-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`
.clusterGrid {
display: grid;
grid-template-columns: ${cssManager.cssGridColumns(3, 8)};
grid-gap: 16px;
margin-bottom: 40px;
}
`,
];
public render() {
return html`
<cloudly-sectionheading>Overview</cloudly-sectionheading>
${this.data.clusters.length === 0 ? html`
You need to create at least one cluster to see an overview.
`: html``}
${this.data.clusters.map(
(clusterArg) => html`
<dees-label .label=${'cluster: ' + clusterArg.data.name}></dees-label>
<div class="clusterGrid">
<dees-chart-area .label=${'System Usage'}></dees-chart-area>
<dees-chart-area .label=${'Internet Traffic'}></dees-chart-area>
<dees-chart-area .label=${'Requests'}></dees-chart-area>
<dees-chart-area .label=${'WebSocket Connections'}></dees-chart-area>
<dees-chart-log class="services" .label=${'Deployed Services'}></dees-chart-log>
<dees-chart-log class="eventLog" .label=${'Event Log'}></dees-chart-log>
</div>
`
)}
`;
}
}

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,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-services')
export class CloudlyViewServices 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>Services</cloudly-sectionheading>
<dees-table
.heading1=${'Services'}
.heading2=${'decoded in client'}
.data=${this.data.services}
.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,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

@@ -0,0 +1,225 @@
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-services')
export class CloudlyViewServices 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`
.category-badge { padding: 2px 8px; border-radius: 4px; font-size: 0.9em; font-weight: 500; }
.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 {
switch (category) {
case 'base': return 'lucide:ServerCog';
case 'distributed': return 'lucide:Network';
case 'workload': return 'lucide:Container';
default: return 'lucide:Box';
}
}
private getCategoryBadgeHtml(category: string): any {
const className = `category-badge category-${category}`;
return html`<span class="${className}">${category}</span>`;
}
private getStrategyBadgeHtml(strategy: string): any {
return html`<span class="strategy-badge">${strategy}</span>`;
}
public render() {
return html`
<cloudly-sectionheading>Services</cloudly-sectionheading>
<dees-table
.heading1=${'Services'}
.heading2=${'Service configuration and deployment management'}
.data=${this.data.services || []}
.displayFunction=${(itemArg: plugins.interfaces.data.IService) => {
return {
Name: itemArg.data.name,
Description: itemArg.data.description,
Category: this.getCategoryBadgeHtml(itemArg.data.serviceCategory || 'workload'),
'Deployment Strategy': html`
${this.getStrategyBadgeHtml(itemArg.data.deploymentStrategy || 'custom')}
${itemArg.data.maxReplicas ? html`<span style="color: #888; margin-left: 8px;">Max: ${itemArg.data.maxReplicas}</span>` : ''}
${itemArg.data.antiAffinity ? html`<span style="color: #f44336; margin-left: 8px;">⚡ Anti-affinity</span>` : ''}
`,
'Image': `${itemArg.data.imageId}:${itemArg.data.imageVersion}`,
'Scale Factor': itemArg.data.scaleFactor,
'Balancing': itemArg.data.balancingStrategy,
'Deployments': itemArg.data.deploymentIds?.length || 0,
};
}}
.dataActions=${[
{
name: 'Add Service',
iconName: 'plus',
type: ['header', 'footer'],
actionFunc: async () => {
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: 'Add Service',
content: html`
<dees-form>
<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-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>
<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>
<dees-input-text .key=${'maxReplicas'} .label=${'Max Replicas (for distributed services)'} .value=${'1'} .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=${'imageVersion'} .label=${'Image Version'} .value=${'latest'} .required=${true}></dees-input-text>
<dees-input-text .key=${'scaleFactor'} .label=${'Scale Factor'} .value=${'1'} .type=${'number'} .required=${true}></dees-input-text>
<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>
<dees-input-text .key=${'webPort'} .label=${'Web Port'} .value=${'80'} .type=${'number'} .required=${true}></dees-input-text>
</dees-form>
`,
menuOptions: [
{ name: 'Create Service', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.createServiceAction, {
serviceData: {
name: formData.name,
description: formData.description,
serviceCategory: formData.serviceCategory,
deploymentStrategy: formData.deploymentStrategy,
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
antiAffinity: formData.antiAffinity,
imageId: formData.imageId,
imageVersion: formData.imageVersion,
scaleFactor: parseInt(formData.scaleFactor),
balancingStrategy: formData.balancingStrategy,
ports: { web: parseInt(formData.webPort) },
environment: {},
domains: [],
deploymentIds: [],
},
});
await modalArg.destroy();
}},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
},
},
{
name: 'Edit',
iconName: 'edit',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
const service = actionDataArg.item as plugins.interfaces.data.IService;
const modal = await plugins.deesCatalog.DeesModal.createAndShow({
heading: `Edit Service: ${service.data.name}`,
content: html`
<dees-form>
<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-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>
<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>
<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=${'scaleFactor'} .label=${'Scale Factor'} .value=${service.data.scaleFactor} .type=${'number'} .required=${true}></dees-input-text>
<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>
</dees-form>
`,
menuOptions: [
{ name: 'Update Service', action: async (modalArg: any) => {
const form = modalArg.shadowRoot.querySelector('dees-form') as any;
const formData = await form.gatherData();
await appstate.dataState.dispatchAction(appstate.updateServiceAction, {
serviceId: service.id,
serviceData: {
...service.data,
name: formData.name,
description: formData.description,
serviceCategory: formData.serviceCategory,
deploymentStrategy: formData.deploymentStrategy,
maxReplicas: formData.maxReplicas ? parseInt(formData.maxReplicas) : undefined,
antiAffinity: formData.antiAffinity,
imageVersion: formData.imageVersion,
scaleFactor: parseInt(formData.scaleFactor),
balancingStrategy: formData.balancingStrategy,
},
});
await modalArg.destroy();
}},
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
],
});
},
},
{
name: 'Deploy',
iconName: 'rocket',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
const service = actionDataArg.item as plugins.interfaces.data.IService;
console.log('Deploy service:', service);
},
},
{
name: 'Delete',
iconName: 'trash',
type: ['contextmenu', 'inRow'],
actionFunc: async (actionDataArg: any) => {
const service = actionDataArg.item as plugins.interfaces.data.IService;
plugins.deesCatalog.DeesModal.createAndShow({
heading: `Delete Service: ${service.data.name}`,
content: html`
<div style="text-align:center">Are you sure you want to delete this service?</div>
<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: #aaa; font-size: 0.9em; margin-top: 4px;">${service.data.description}</div>
<div style="color: #f44336; margin-top: 8px;">This will also delete ${service.data.deploymentIds?.length || 0} deployment(s)</div>
</div>
`,
menuOptions: [
{ name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() },
{ name: 'Delete', action: async (modalArg: any) => { await appstate.dataState.dispatchAction(appstate.deleteServiceAction, { serviceId: service.id }); await modalArg.destroy(); } },
],
});
},
},
] as plugins.deesCatalog.ITableAction[]}
></dees-table>
`;
}
}
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

@@ -18,4 +18,8 @@ import * as smartstate from '@push.rocks/smartstate';
export { export {
webjwt, webjwt,
smartstate, smartstate,
} }
// Expose API client so UI can share it with CLI
import * as servezoneApi from '@serve.zone/api';
export { servezoneApi };