ui rebuild

This commit is contained in:
2025-11-24 19:52:35 +00:00
parent c9beae93c8
commit 9aa6906ca5
73 changed files with 8514 additions and 4537 deletions

View File

@@ -40,8 +40,8 @@ export class OneboxDockerManager {
*/
private async ensureNetwork(): Promise<void> {
try {
const networks = await this.dockerClient!.getNetworks();
const existingNetwork = networks.find((n: any) => n.name === this.networkName);
const networks = await this.dockerClient!.listNetworks();
const existingNetwork = networks.find((n: any) => n.Name === this.networkName);
if (!existingNetwork) {
logger.info(`Creating Docker network: ${this.networkName}`);
@@ -228,14 +228,12 @@ export class OneboxDockerManager {
* Get network ID by name
*/
private async getNetworkID(networkName: string): Promise<string> {
const networks = await this.dockerClient!.getNetworks();
const network = networks.find((n: any) =>
(n.name || n.Name) === networkName
);
const networks = await this.dockerClient!.listNetworks();
const network = networks.find((n: any) => n.Name === networkName);
if (!network) {
throw new Error(`Network not found: ${networkName}`);
}
return network.id || network.Id;
return network.Id;
}
/**
@@ -578,19 +576,32 @@ export class OneboxDockerManager {
*/
async getContainerStats(containerID: string): Promise<IContainerStats | null> {
try {
const container = this.dockerClient!.getContainer(containerID);
const container = await this.dockerClient!.getContainerById(containerID);
if (!container) {
// Container not found - this is expected for Swarm services where we have service ID instead of container ID
// Return null silently
return null;
}
const stats = await container.stats({ stream: false });
// Validate stats structure
if (!stats || !stats.cpu_stats || !stats.cpu_stats.cpu_usage) {
logger.warn(`Invalid stats structure for container ${containerID}`);
return null;
}
// Calculate CPU percentage
const cpuDelta =
stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
stats.cpu_stats.cpu_usage.total_usage - (stats.precpu_stats?.cpu_usage?.total_usage || 0);
const systemDelta = stats.cpu_stats.system_cpu_usage - (stats.precpu_stats?.system_cpu_usage || 0);
const cpuPercent =
systemDelta > 0 ? (cpuDelta / systemDelta) * stats.cpu_stats.online_cpus * 100 : 0;
// Memory stats
const memoryUsed = stats.memory_stats.usage || 0;
const memoryLimit = stats.memory_stats.limit || 0;
const memoryUsed = stats.memory_stats?.usage || 0;
const memoryLimit = stats.memory_stats?.limit || 0;
const memoryPercent = memoryLimit > 0 ? (memoryUsed / memoryLimit) * 100 : 0;
// Network stats
@@ -612,49 +623,88 @@ export class OneboxDockerManager {
networkTx,
};
} catch (error) {
logger.error(`Failed to get container stats ${containerID}: ${error.message}`);
// Don't log errors for container not found - this is expected for Swarm services
if (!error.message.includes('No such container') && !error.message.includes('not found')) {
logger.error(`Failed to get container stats ${containerID}: ${error.message}`);
}
return null;
}
}
/**
* Helper: Get actual container ID for a Swarm service
* For Swarm services, we need to find the task/container that's actually running
*/
private async getContainerIdForService(serviceId: string): Promise<string | null> {
try {
// List all containers and find one with the service label matching our service ID
const containers = await this.dockerClient!.listContainers();
// Find a container that belongs to this service
const serviceContainer = containers.find((container: any) => {
const labels = container.Labels || {};
// Swarm services have a com.docker.swarm.service.id label
return labels['com.docker.swarm.service.id'] === serviceId;
});
if (serviceContainer) {
return serviceContainer.Id;
}
return null;
} catch (error) {
logger.warn(`Failed to get container ID for service ${serviceId}: ${error.message}`);
return null;
}
}
/**
* Get container logs
* Handles both regular containers and Swarm services
*/
async getContainerLogs(
containerID: string,
tail = 100
): Promise<{ stdout: string; stderr: string }> {
try {
const container = this.dockerClient!.getContainer(containerID);
const logs = await container.logs({
stdout: true,
stderr: true,
tail,
timestamps: true,
});
let actualContainerId = containerID;
// Parse logs (Docker returns them in a special format)
const stdout: string[] = [];
const stderr: string[] = [];
// Try to get container directly first
let container = await this.dockerClient!.getContainerById(containerID);
const lines = logs.toString().split('\n');
for (const line of lines) {
if (line.length === 0) continue;
// Docker log format: first byte indicates stream (1=stdout, 2=stderr)
const streamType = line.charCodeAt(0);
const content = line.slice(8); // Skip header (8 bytes)
if (streamType === 1) {
stdout.push(content);
} else if (streamType === 2) {
stderr.push(content);
// If not found, it might be a service ID - try to get the actual container ID
if (!container) {
const serviceContainerId = await this.getContainerIdForService(containerID);
if (serviceContainerId) {
actualContainerId = serviceContainerId;
container = await this.dockerClient!.getContainerById(serviceContainerId);
}
}
if (!container) {
throw new Error(`Container not found: ${containerID}`);
}
// Get logs as string (v5 handles demultiplexing automatically)
const logs = await container.logs({
stdout: true,
stderr: true,
tail: tail,
timestamps: true,
});
// v5 should return a string, but let's handle edge cases
if (typeof logs !== 'string') {
logger.error(`Unexpected logs type: ${typeof logs}, constructor: ${logs?.constructor?.name}`);
logger.error(`Logs content: ${JSON.stringify(logs).slice(0, 500)}`);
// If it's not a string, something went wrong
throw new Error(`Unexpected log format: expected string, got ${typeof logs}`);
}
// v5 returns already-parsed logs as a string
return {
stdout: stdout.join('\n'),
stderr: stderr.join('\n'),
stdout: logs,
stderr: '', // v5 combines stdout/stderr into single string
};
} catch (error) {
logger.error(`Failed to get container logs ${containerID}: ${error.message}`);
@@ -662,47 +712,15 @@ export class OneboxDockerManager {
}
}
/**
* Stream container logs (real-time)
*/
async streamContainerLogs(
containerID: string,
callback: (line: string, isError: boolean) => void
): Promise<void> {
try {
const container = this.dockerClient!.getContainer(containerID);
const stream = await container.logs({
stdout: true,
stderr: true,
follow: true,
tail: 0,
timestamps: true,
});
stream.on('data', (chunk: Buffer) => {
const streamType = chunk[0];
const content = chunk.slice(8).toString();
callback(content, streamType === 2);
});
stream.on('error', (error: Error) => {
logger.error(`Log stream error for ${containerID}: ${error.message}`);
});
} catch (error) {
logger.error(`Failed to stream container logs ${containerID}: ${error.message}`);
throw error;
}
}
/**
* List all onebox-managed containers
*/
async listContainers(): Promise<any[]> {
try {
const containers = await this.dockerClient!.getContainers();
const containers = await this.dockerClient!.listContainers();
// Filter for onebox-managed containers
return containers.filter((c: any) =>
c.labels && c.labels['managed-by'] === 'onebox'
c.Labels && c.Labels['managed-by'] === 'onebox'
);
} catch (error) {
logger.error(`Failed to list containers: ${error.message}`);
@@ -724,14 +742,16 @@ export class OneboxDockerManager {
/**
* Get Docker version info
* Note: v5 API doesn't expose version() method, so we return a placeholder
*/
async getDockerVersion(): Promise<any> {
try {
return await this.dockerClient!.version();
} catch (error) {
logger.error(`Failed to get Docker version: ${error.message}`);
return null;
}
// v5 API doesn't have a version() method
// Return a basic structure for compatibility
return {
Version: 'N/A',
ApiVersion: 'N/A',
Note: 'Version info not available in @apiclient.xyz/docker v5'
};
}
/**
@@ -753,7 +773,12 @@ export class OneboxDockerManager {
*/
async getContainerIP(containerID: string): Promise<string | null> {
try {
const container = this.dockerClient!.getContainer(containerID);
const container = await this.dockerClient!.getContainerById(containerID);
if (!container) {
throw new Error(`Container not found: ${containerID}`);
}
const info = await container.inspect();
const networks = info.NetworkSettings.Networks;
@@ -776,7 +801,11 @@ export class OneboxDockerManager {
cmd: string[]
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
try {
const container = this.dockerClient!.getContainer(containerID);
const container = await this.dockerClient!.getContainerById(containerID);
if (!container) {
throw new Error(`Container not found: ${containerID}`);
}
const exec = await container.exec({
Cmd: cmd,