feat: integrate toast notifications in settings and layout components

- Added ToastService for managing toast notifications.
- Replaced alert in settings component with toast notifications for success and error messages.
- Included ToastComponent in layout for displaying notifications.
- Created loading spinner component for better user experience.
- Implemented domain detail component with detailed views for certificates, requirements, and services.
- Added functionality to manage and display SSL certificates and their statuses.
- Introduced a registry manager class for handling Docker registry operations.
This commit is contained in:
2025-11-24 01:31:15 +00:00
parent b6ac4f209a
commit c9beae93c8
23 changed files with 2475 additions and 130 deletions

View File

@@ -33,10 +33,26 @@ export class OneboxServicesManager {
throw new Error(`Service already exists: ${options.name}`);
}
// Handle Onebox Registry setup
let registryToken: string | undefined;
let imageToPull: string;
if (options.useOneboxRegistry) {
// Generate registry token
registryToken = await this.oneboxRef.registry.createServiceToken(options.name);
// Use onebox registry image name
const tag = options.registryImageTag || 'latest';
imageToPull = this.oneboxRef.registry.getImageName(options.name, tag);
} else {
// Use external image
imageToPull = options.image;
}
// Create service record in database
const service = await this.database.createService({
name: options.name,
image: options.image,
image: options.useOneboxRegistry ? imageToPull : options.image,
registry: options.registry,
envVars: options.envVars || {},
port: options.port,
@@ -44,10 +60,18 @@ export class OneboxServicesManager {
status: 'stopped',
createdAt: Date.now(),
updatedAt: Date.now(),
// Onebox Registry fields
useOneboxRegistry: options.useOneboxRegistry,
registryRepository: options.useOneboxRegistry ? options.name : undefined,
registryToken: registryToken,
registryImageTag: options.registryImageTag || 'latest',
autoUpdateOnPush: options.autoUpdateOnPush,
});
// Pull image
await this.docker.pullImage(options.image, options.registry);
// Pull image (skip if using onebox registry - image might not exist yet)
if (!options.useOneboxRegistry) {
await this.docker.pullImage(imageToPull, options.registry);
}
// Create container
const containerID = await this.docker.createContainer(service);
@@ -68,6 +92,47 @@ export class OneboxServicesManager {
if (options.domain) {
logger.info(`Configuring domain: ${options.domain}`);
// Validate domain and create CertRequirement
try {
// Extract base domain (e.g., "api.example.com" -> "example.com")
const domainParts = options.domain.split('.');
const baseDomain = domainParts.slice(-2).join('.');
const subdomain = domainParts.length > 2 ? domainParts.slice(0, -2).join('.') : '';
// Check if base domain exists in Domain table
const domainRecord = this.database.getDomainByName(baseDomain);
if (!domainRecord) {
logger.warn(
`Domain ${baseDomain} not found in Domain table. ` +
`Service will deploy but certificate management may not work. ` +
`Run Cloudflare domain sync or manually add the domain.`
);
} else if (domainRecord.isObsolete) {
logger.warn(
`Domain ${baseDomain} is marked as obsolete. ` +
`Certificate management may not work properly.`
);
} else {
// Create CertRequirement for automatic certificate management
const now = Date.now();
this.database.createCertRequirement({
serviceId: service.id!,
domainId: domainRecord.id!,
subdomain: subdomain,
status: 'pending',
createdAt: now,
updatedAt: now,
});
logger.info(
`Created certificate requirement for ${options.domain} ` +
`(domain: ${baseDomain}, subdomain: ${subdomain || 'none'})`
);
}
} catch (error) {
logger.warn(`Failed to create certificate requirement: ${error.message}`);
}
// Configure DNS (if autoDNS is enabled)
if (options.autoDNS !== false) {
try {
@@ -85,6 +150,8 @@ export class OneboxServicesManager {
}
// Configure SSL (if autoSSL is enabled)
// Note: With CertRequirement system, certificates are managed automatically
// but we still support the old direct obtainCertificate for backward compatibility
if (options.autoSSL !== false) {
try {
await this.oneboxRef.ssl.obtainCertificate(options.domain);
@@ -362,6 +429,121 @@ export class OneboxServicesManager {
}
}
/**
* Update service configuration (image, port, domain, env vars)
* Recreates the container with new configuration and auto-restarts
*/
async updateService(
name: string,
updates: {
image?: string;
registry?: string;
port?: number;
domain?: string;
envVars?: Record<string, string>;
}
): Promise<IService> {
try {
const service = this.database.getServiceByName(name);
if (!service) {
throw new Error(`Service not found: ${name}`);
}
logger.info(`Updating service: ${name}`);
const wasRunning = service.status === 'running';
const oldContainerID = service.containerID;
const oldDomain = service.domain;
// Stop the container if running
if (wasRunning && oldContainerID) {
logger.info(`Stopping service ${name} for updates...`);
try {
await this.docker.stopContainer(oldContainerID);
} catch (error) {
logger.warn(`Failed to stop container: ${error.message}`);
}
}
// Pull new image if changed
if (updates.image && updates.image !== service.image) {
logger.info(`Pulling new image: ${updates.image}`);
await this.docker.pullImage(updates.image, updates.registry || service.registry);
}
// Update service in database
const updateData: any = {
updatedAt: Date.now(),
};
if (updates.image !== undefined) updateData.image = updates.image;
if (updates.registry !== undefined) updateData.registry = updates.registry;
if (updates.port !== undefined) updateData.port = updates.port;
if (updates.domain !== undefined) updateData.domain = updates.domain;
if (updates.envVars !== undefined) updateData.envVars = updates.envVars;
this.database.updateService(service.id!, updateData);
// Get updated service
const updatedService = this.database.getServiceByName(name)!;
// Remove old container
if (oldContainerID) {
try {
await this.docker.removeContainer(oldContainerID, true);
logger.info(`Removed old container for ${name}`);
} catch (error) {
logger.warn(`Failed to remove old container: ${error.message}`);
}
}
// Create new container with updated config
logger.info(`Creating new container for ${name}...`);
const containerID = await this.docker.createContainer(updatedService);
this.database.updateService(service.id!, { containerID });
// Update reverse proxy if domain changed
if (updates.domain !== undefined && updates.domain !== oldDomain) {
// Remove old route if it existed
if (oldDomain) {
try {
this.oneboxRef.reverseProxy.removeRoute(oldDomain);
} catch (error) {
logger.warn(`Failed to remove old reverse proxy route: ${error.message}`);
}
}
// Add new route if domain specified
if (updates.domain) {
try {
await this.oneboxRef.reverseProxy.addRoute(
service.id!,
updates.domain,
updates.port || service.port
);
} catch (error) {
logger.warn(`Failed to configure reverse proxy: ${error.message}`);
}
}
}
// Restart the container if it was running
if (wasRunning) {
logger.info(`Starting updated service ${name}...`);
this.database.updateService(service.id!, { status: 'starting' });
await this.docker.startContainer(containerID);
this.database.updateService(service.id!, { status: 'running' });
logger.success(`Service ${name} updated and restarted`);
} else {
this.database.updateService(service.id!, { status: 'stopped' });
logger.success(`Service ${name} updated (not started)`);
}
return this.database.getServiceByName(name)!;
} catch (error) {
logger.error(`Failed to update service ${name}: ${error.message}`);
throw error;
}
}
/**
* Sync service status from Docker
*/
@@ -410,4 +592,86 @@ export class OneboxServicesManager {
await this.syncServiceStatus(service.name);
}
}
/**
* Start auto-update monitoring for registry services
* Polls every 30 seconds for digest changes and restarts services if needed
*/
startAutoUpdateMonitoring(): void {
// Check every 30 seconds
setInterval(async () => {
try {
await this.checkForRegistryUpdates();
} catch (error) {
logger.error(`Auto-update check failed: ${error.message}`);
}
}, 30000);
logger.info('Auto-update monitoring started (30s interval)');
}
/**
* Check all services using onebox registry for updates
*/
private async checkForRegistryUpdates(): Promise<void> {
const services = this.listServices();
for (const service of services) {
// Skip if not using onebox registry or auto-update is disabled
if (!service.useOneboxRegistry || !service.autoUpdateOnPush) {
continue;
}
try {
// Get current digest from registry
const currentDigest = await this.oneboxRef.registry.getImageDigest(
service.registryRepository!,
service.registryImageTag || 'latest'
);
// Skip if no digest found (image might not exist yet)
if (!currentDigest) {
continue;
}
// Check if digest has changed
if (service.imageDigest && service.imageDigest !== currentDigest) {
logger.info(
`Digest changed for ${service.name}: ${service.imageDigest} -> ${currentDigest}`
);
// Update digest in database
this.database.updateService(service.id!, {
imageDigest: currentDigest,
});
// Pull new image
const imageName = this.oneboxRef.registry.getImageName(
service.registryRepository!,
service.registryImageTag || 'latest'
);
logger.info(`Pulling updated image: ${imageName}`);
await this.docker.pullImage(imageName);
// Restart service
logger.info(`Auto-restarting service: ${service.name}`);
await this.restartService(service.name);
// Broadcast update via WebSocket
this.oneboxRef.httpServer.broadcastServiceUpdate({
action: 'updated',
service: this.database.getServiceByName(service.name)!,
});
} else if (!service.imageDigest) {
// First time - just store the digest
this.database.updateService(service.id!, {
imageDigest: currentDigest,
});
}
} catch (error) {
logger.error(`Failed to check updates for ${service.name}: ${error.message}`);
}
}
}
}