Compare commits

...

12 Commits

Author SHA1 Message Date
c420a30341 v1.18.2
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 28s
CI / Build Test (Current Platform) (push) Successful in 59s
CI / Build All Platforms (push) Successful in 2m0s
Release / build-and-release (push) Successful in 3m41s
2026-03-16 14:14:55 +00:00
fe109f0953 fix(repo): no changes to commit 2026-03-16 14:14:55 +00:00
012dce63b1 v1.18.1
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
Release / build-and-release (push) Successful in 4m0s
2026-03-16 14:14:34 +00:00
54780482c7 fix(repo): no changes to commit 2026-03-16 14:14:34 +00:00
7ab0fb3c1f v1.18.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 27s
CI / Build Test (Current Platform) (push) Successful in 58s
CI / Build All Platforms (push) Successful in 1m52s
Release / build-and-release (push) Successful in 2m58s
2026-03-16 13:51:43 +00:00
713fda2a86 feat(platform-services): add platform service log retrieval and display in the services UI 2026-03-16 13:51:43 +00:00
ec32c19300 v1.17.4
Some checks failed
CI / Type Check & Lint (push) Failing after 30s
Publish to npm / npm-publish (push) Failing after 24s
CI / Build Test (Current Platform) (push) Successful in 1m1s
CI / Build All Platforms (push) Successful in 2m12s
Release / build-and-release (push) Successful in 4m0s
2026-03-16 13:26:56 +00:00
7d1d91157c fix(docs): add hello world running screenshot for documentation 2026-03-16 13:26:56 +00:00
b69c96c240 v1.17.3
Some checks failed
CI / Build Test (Current Platform) (push) Failing after 6s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 32s
CI / Build All Platforms (push) Successful in 2m4s
Release / build-and-release (push) Successful in 3m13s
2026-03-16 13:05:47 +00:00
9ee8851d03 fix(mongodb): downgrade the MongoDB service image to 4.4 and use the legacy mongo shell for container operations 2026-03-16 13:05:47 +00:00
7f6031f31a v1.17.2
Some checks failed
CI / Type Check & Lint (push) Failing after 29s
Publish to npm / npm-publish (push) Failing after 25s
CI / Build Test (Current Platform) (push) Successful in 1m4s
CI / Build All Platforms (push) Successful in 2m5s
Release / build-and-release (push) Successful in 4m12s
2026-03-16 12:45:44 +00:00
6f1b8469e0 fix(platform-services): provision ClickHouse, MinIO, and MongoDB resources via docker exec instead of host port access 2026-03-16 12:45:44 +00:00
18 changed files with 254 additions and 266 deletions

View File

@@ -1,5 +1,39 @@
# Changelog # Changelog
## 2026-03-16 - 1.18.2 - fix(repo)
no changes to commit
## 2026-03-16 - 1.18.1 - fix(repo)
no changes to commit
## 2026-03-16 - 1.18.0 - feat(platform-services)
add platform service log retrieval and display in the services UI
- add typed request support in the ops server to fetch Docker logs for platform service containers
- store fetched platform service logs in web app state and load them when opening platform service details
- render platform service logs in the services detail view and add sidebar icons for main navigation tabs
## 2026-03-16 - 1.17.4 - fix(docs)
add hello world running screenshot for documentation
- Adds a new PNG asset showing the application in a running hello world state.
- Supports project documentation or README usage without changing runtime behavior.
## 2026-03-16 - 1.17.3 - fix(mongodb)
downgrade the MongoDB service image to 4.4 and use the legacy mongo shell for container operations
- changes the default MongoDB container image from mongo:7 to mongo:4.4
- replaces mongosh with mongo for health checks, provisioning, and deprovisioning inside the container
## 2026-03-16 - 1.17.2 - fix(platform-services)
provision ClickHouse, MinIO, and MongoDB resources via docker exec instead of host port access
- switch ClickHouse provisioning and teardown to in-container client commands to avoid host port mapping issues
- replace MinIO host-side S3 API calls with in-container mc commands for bucket creation and removal
- run MongoDB provisioning and deprovisioning through mongosh inside the container and improve docker exec failure reporting
## 2026-03-16 - 1.17.1 - fix(repo) ## 2026-03-16 - 1.17.1 - fix(repo)
no changes to commit no changes to commit

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.17.1", "version": "1.18.2",
"exports": "./mod.ts", "exports": "./mod.ts",
"tasks": { "tasks": {
"test": "deno test --allow-all test/", "test": "deno test --allow-all test/",

BIN
hello-world-running.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.17.1", "version": "1.18.2",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers", "description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts", "main": "mod.ts",
"type": "module", "type": "module",

BIN
sidebar-icons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/onebox', name: '@serve.zone/onebox',
version: '1.17.1', version: '1.18.2',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
} }

View File

@@ -881,12 +881,12 @@ export class OneboxDockerManager {
]); ]);
const execInfo = await inspect(); const execInfo = await inspect();
const exitCode = execInfo.ExitCode || 0; const exitCode = execInfo.ExitCode ?? -1;
return { stdout, stderr, exitCode }; return { stdout, stderr, exitCode };
} catch (error) { } catch (error) {
logger.error(`Failed to exec in container ${containerID}: ${getErrorMessage(error)}`); logger.error(`Failed to exec in container ${containerID}: ${getErrorMessage(error)}`);
throw error; return { stdout: '', stderr: getErrorMessage(error), exitCode: -1 };
} }
} }

View File

@@ -194,12 +194,6 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName(); const containerName = this.getContainerName();
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 8123);
if (!hostPort) {
throw new Error('Could not get ClickHouse container host port');
}
// Generate resource names and credentials // Generate resource names and credentials
const dbName = this.generateResourceName(userService.name); const dbName = this.generateResourceName(userService.name);
const username = this.generateResourceName(userService.name); const username = this.generateResourceName(userService.name);
@@ -207,35 +201,16 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
logger.info(`Provisioning ClickHouse database '${dbName}' for service '${userService.name}'...`); logger.info(`Provisioning ClickHouse database '${dbName}' for service '${userService.name}'...`);
// Connect to ClickHouse via localhost and the mapped host port // Use docker exec to provision inside the container (avoids host port mapping issues)
const baseUrl = `http://127.0.0.1:${hostPort}`; const queries = [
`CREATE DATABASE IF NOT EXISTS ${dbName}`,
`CREATE USER IF NOT EXISTS ${username} IDENTIFIED BY '${password}'`,
`GRANT ALL ON ${dbName}.* TO ${username}`,
];
// Create database for (const query of queries) {
await this.executeQuery( await this.execClickHouseQuery(platformService.containerId, adminCreds, query);
baseUrl, }
adminCreds.username,
adminCreds.password,
`CREATE DATABASE IF NOT EXISTS ${dbName}`
);
logger.info(`Created ClickHouse database '${dbName}'`);
// Create user with access to this database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`CREATE USER IF NOT EXISTS ${username} IDENTIFIED BY '${password}'`
);
logger.info(`Created ClickHouse user '${username}'`);
// Grant permissions on the database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`GRANT ALL ON ${dbName}.* TO ${username}`
);
logger.info(`Granted permissions to user '${username}' on database '${dbName}'`);
logger.success(`ClickHouse database '${dbName}' provisioned with user '${username}'`); logger.success(`ClickHouse database '${dbName}' provisioned with user '${username}'`);
@@ -274,37 +249,11 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 8123);
if (!hostPort) {
throw new Error('Could not get ClickHouse container host port');
}
logger.info(`Deprovisioning ClickHouse database '${resource.resourceName}'...`); logger.info(`Deprovisioning ClickHouse database '${resource.resourceName}'...`);
const baseUrl = `http://127.0.0.1:${hostPort}`;
try { try {
// Drop the user await this.execClickHouseQuery(platformService.containerId, adminCreds, `DROP USER IF EXISTS ${credentials.username}`);
try { await this.execClickHouseQuery(platformService.containerId, adminCreds, `DROP DATABASE IF EXISTS ${resource.resourceName}`);
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`DROP USER IF EXISTS ${credentials.username}`
);
logger.info(`Dropped ClickHouse user '${credentials.username}'`);
} catch (e) {
logger.warn(`Could not drop ClickHouse user: ${getErrorMessage(e)}`);
}
// Drop the database
await this.executeQuery(
baseUrl,
adminCreds.username,
adminCreds.password,
`DROP DATABASE IF EXISTS ${resource.resourceName}`
);
logger.success(`ClickHouse database '${resource.resourceName}' dropped`); logger.success(`ClickHouse database '${resource.resourceName}' dropped`);
} catch (e) { } catch (e) {
logger.error(`Failed to deprovision ClickHouse database: ${getErrorMessage(e)}`); logger.error(`Failed to deprovision ClickHouse database: ${getErrorMessage(e)}`);
@@ -313,26 +262,27 @@ export class ClickHouseProvider extends BasePlatformServiceProvider {
} }
/** /**
* Execute a ClickHouse SQL query via HTTP interface * Execute a ClickHouse SQL query via docker exec inside the container
*/ */
private async executeQuery( private async execClickHouseQuery(
baseUrl: string, containerId: string,
username: string, adminCreds: { username: string; password: string },
password: string,
query: string query: string
): Promise<string> { ): Promise<string> {
const url = `${baseUrl}/?user=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`; const result = await this.oneboxRef.docker.execInContainer(
containerId,
[
'clickhouse-client',
'--user', adminCreds.username,
'--password', adminCreds.password,
'--query', query,
]
);
const response = await fetch(url, { if (result.exitCode !== 0) {
method: 'POST', throw new Error(`ClickHouse query failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`);
body: query,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`ClickHouse query failed: ${errorText}`);
} }
return await response.text(); return result.stdout;
} }
} }

View File

@@ -196,84 +196,28 @@ export class MinioProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName(); const containerName = this.getContainerName();
// Get container host port for connection from host (overlay network IPs not accessible from host) // Generate bucket name
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000);
if (!hostPort) {
throw new Error('Could not get MinIO container host port');
}
// Generate bucket name and credentials
const bucketName = this.generateBucketName(userService.name); const bucketName = this.generateBucketName(userService.name);
const accessKey = credentialEncryption.generateAccessKey(20);
const secretKey = credentialEncryption.generateSecretKey(40);
logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`); logger.info(`Provisioning MinIO bucket '${bucketName}' for service '${userService.name}'...`);
// Connect to MinIO via localhost and the mapped host port (for provisioning from host) // Use docker exec with mc (MinIO Client) inside the container
const provisioningEndpoint = `http://127.0.0.1:${hostPort}`; // First configure mc alias for local server
await this.execMc(platformService.containerId, [
// Import AWS S3 client 'alias', 'set', 'local', 'http://localhost:9000',
const { S3Client, CreateBucketCommand, PutBucketPolicyCommand } = await import('npm:@aws-sdk/client-s3@3'); adminCreds.username, adminCreds.password,
]);
// Create S3 client with admin credentials - connect via host port
const s3Client = new S3Client({
endpoint: provisioningEndpoint,
region: 'us-east-1',
credentials: {
accessKeyId: adminCreds.username,
secretAccessKey: adminCreds.password,
},
forcePathStyle: true,
});
// Create the bucket // Create the bucket
try { const mbResult = await this.execMc(platformService.containerId, [
await s3Client.send(new CreateBucketCommand({ 'mb', '--ignore-existing', `local/${bucketName}`,
Bucket: bucketName, ]);
})); logger.info(`Created MinIO bucket '${bucketName}'`);
logger.info(`Created MinIO bucket '${bucketName}'`);
} catch (e: any) {
if (e.name !== 'BucketAlreadyOwnedByYou' && e.name !== 'BucketAlreadyExists') {
throw e;
}
logger.warn(`Bucket '${bucketName}' already exists`);
}
// Create service account/access key using MinIO Admin API // Set bucket policy to allow public read/write (services on the same network use root creds)
// MinIO Admin API requires mc client or direct API calls await this.execMc(platformService.containerId, [
// For simplicity, we'll use root credentials and bucket policy isolation 'anonymous', 'set', 'none', `local/${bucketName}`,
// In production, you'd use MinIO's Admin API to create service accounts ]);
// Set bucket policy to allow access only with this bucket's credentials
const bucketPolicy = {
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: { AWS: ['*'] },
Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:ListBucket'],
Resource: [
`arn:aws:s3:::${bucketName}`,
`arn:aws:s3:::${bucketName}/*`,
],
},
],
};
try {
await s3Client.send(new PutBucketPolicyCommand({
Bucket: bucketName,
Policy: JSON.stringify(bucketPolicy),
}));
logger.info(`Set bucket policy for '${bucketName}'`);
} catch (e) {
logger.warn(`Could not set bucket policy: ${getErrorMessage(e)}`);
}
// Note: For proper per-service credentials, MinIO Admin API should be used
// For now, we're providing the bucket with root access
// TODO: Implement MinIO service account creation
logger.warn('Using root credentials for MinIO access. Consider implementing service accounts for production.');
// Use container name for the endpoint in credentials (user services run in same network) // Use container name for the endpoint in credentials (user services run in same network)
const serviceEndpoint = `http://${containerName}:9000`; const serviceEndpoint = `http://${containerName}:9000`;
@@ -281,7 +225,7 @@ export class MinioProvider extends BasePlatformServiceProvider {
const credentials: Record<string, string> = { const credentials: Record<string, string> = {
endpoint: serviceEndpoint, endpoint: serviceEndpoint,
bucket: bucketName, bucket: bucketName,
accessKey: adminCreds.username, // Using root for now accessKey: adminCreds.username,
secretKey: adminCreds.password, secretKey: adminCreds.password,
region: 'us-east-1', region: 'us-east-1',
}; };
@@ -312,57 +256,37 @@ export class MinioProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 9000);
if (!hostPort) {
throw new Error('Could not get MinIO container host port');
}
logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`); logger.info(`Deprovisioning MinIO bucket '${resource.resourceName}'...`);
const { S3Client, DeleteBucketCommand, ListObjectsV2Command, DeleteObjectsCommand } = await import('npm:@aws-sdk/client-s3@3'); // Configure mc alias
await this.execMc(platformService.containerId, [
const s3Client = new S3Client({ 'alias', 'set', 'local', 'http://localhost:9000',
endpoint: `http://127.0.0.1:${hostPort}`, adminCreds.username, adminCreds.password,
region: 'us-east-1', ]);
credentials: {
accessKeyId: adminCreds.username,
secretAccessKey: adminCreds.password,
},
forcePathStyle: true,
});
try { try {
// First, delete all objects in the bucket // Remove all objects and the bucket
let continuationToken: string | undefined; await this.execMc(platformService.containerId, [
do { 'rb', '--force', `local/${resource.resourceName}`,
const listResponse = await s3Client.send(new ListObjectsV2Command({ ]);
Bucket: resource.resourceName,
ContinuationToken: continuationToken,
}));
if (listResponse.Contents && listResponse.Contents.length > 0) {
await s3Client.send(new DeleteObjectsCommand({
Bucket: resource.resourceName,
Delete: {
Objects: listResponse.Contents.map(obj => ({ Key: obj.Key! })),
},
}));
logger.info(`Deleted ${listResponse.Contents.length} objects from bucket`);
}
continuationToken = listResponse.IsTruncated ? listResponse.NextContinuationToken : undefined;
} while (continuationToken);
// Now delete the bucket
await s3Client.send(new DeleteBucketCommand({
Bucket: resource.resourceName,
}));
logger.success(`MinIO bucket '${resource.resourceName}' deleted`); logger.success(`MinIO bucket '${resource.resourceName}' deleted`);
} catch (e) { } catch (e) {
logger.error(`Failed to delete MinIO bucket: ${getErrorMessage(e)}`); logger.error(`Failed to delete MinIO bucket: ${getErrorMessage(e)}`);
throw e; throw e;
} }
} }
/**
* Execute mc (MinIO Client) command inside the container
*/
private async execMc(
containerId: string,
args: string[],
): Promise<{ stdout: string; stderr: string }> {
const result = await this.oneboxRef.docker.execInContainer(containerId, ['mc', ...args]);
if (result.exitCode !== 0) {
throw new Error(`mc command failed (exit ${result.exitCode}): ${result.stderr.substring(0, 200)}`);
}
return result;
}
} }

View File

@@ -28,7 +28,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
getDefaultConfig(): IPlatformServiceConfig { getDefaultConfig(): IPlatformServiceConfig {
return { return {
image: 'mongo:7', image: 'mongo:4.4',
port: 27017, port: 27017,
volumes: ['/var/lib/onebox/mongodb:/data/db'], volumes: ['/var/lib/onebox/mongodb:/data/db'],
environment: { environment: {
@@ -165,7 +165,7 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
// This avoids network issues with overlay networks // This avoids network issues with overlay networks
const result = await this.oneboxRef.docker.execInContainer( const result = await this.oneboxRef.docker.execInContainer(
platformService.containerId, platformService.containerId,
['mongosh', '--eval', 'db.adminCommand("ping")', '--username', adminCreds.username, '--password', adminCreds.password, '--authenticationDatabase', 'admin', '--quiet'] ['mongo', '--eval', 'db.adminCommand("ping")', '--username', adminCreds.username, '--password', adminCreds.password, '--authenticationDatabase', 'admin', '--quiet']
); );
if (result.exitCode === 0) { if (result.exitCode === 0) {
@@ -190,12 +190,6 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const containerName = this.getContainerName(); const containerName = this.getContainerName();
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 27017);
if (!hostPort) {
throw new Error('Could not get MongoDB container host port');
}
// Generate resource names and credentials // Generate resource names and credentials
const dbName = this.generateResourceName(userService.name); const dbName = this.generateResourceName(userService.name);
const username = this.generateResourceName(userService.name); const username = this.generateResourceName(userService.name);
@@ -203,32 +197,40 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
logger.info(`Provisioning MongoDB database '${dbName}' for service '${userService.name}'...`); logger.info(`Provisioning MongoDB database '${dbName}' for service '${userService.name}'...`);
// Connect to MongoDB via localhost and the mapped host port // Use docker exec to provision inside the container (avoids host port mapping issues)
const { MongoClient } = await import('npm:mongodb@6'); const escapedPassword = password.replace(/'/g, "'\\''");
const adminUri = `mongodb://${adminCreds.username}:${adminCreds.password}@127.0.0.1:${hostPort}/?authSource=admin`; const escapedAdminPassword = adminCreds.password.replace(/'/g, "'\\''");
const client = new MongoClient(adminUri); // Create database and user via mongo inside the container
await client.connect(); const mongoScript = `
db = db.getSiblingDB('${dbName}');
try { db.createCollection('_onebox_init');
// Create the database by switching to it (MongoDB creates on first write) db.createUser({
const db = client.db(dbName); user: '${username}',
pwd: '${escapedPassword}',
// Create a collection to ensure the database exists roles: [{ role: 'readWrite', db: '${dbName}' }]
await db.createCollection('_onebox_init');
// Create user with readWrite access to this database
await db.command({
createUser: username,
pwd: password,
roles: [{ role: 'readWrite', db: dbName }],
}); });
print('PROVISION_SUCCESS');
`;
logger.success(`MongoDB database '${dbName}' provisioned with user '${username}'`); const result = await this.oneboxRef.docker.execInContainer(
} finally { platformService.containerId,
await client.close(); [
'mongo',
'--username', adminCreds.username,
'--password', escapedAdminPassword,
'--authenticationDatabase', 'admin',
'--quiet',
'--eval', mongoScript,
]
);
if (result.exitCode !== 0 || !result.stdout.includes('PROVISION_SUCCESS')) {
throw new Error(`Failed to provision MongoDB database: exit code ${result.exitCode}, output: ${result.stdout.substring(0, 200)} ${result.stderr.substring(0, 200)}`);
} }
logger.success(`MongoDB database '${dbName}' provisioned with user '${username}'`);
// Build the credentials and env vars // Build the credentials and env vars
const credentials: Record<string, string> = { const credentials: Record<string, string> = {
host: containerName, host: containerName,
@@ -262,37 +264,33 @@ export class MongoDBProvider extends BasePlatformServiceProvider {
} }
const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted); const adminCreds = await credentialEncryption.decrypt(platformService.adminCredentialsEncrypted);
const escapedAdminPassword = adminCreds.password.replace(/'/g, "'\\''");
// Get container host port for connection from host (overlay network IPs not accessible from host)
const hostPort = await this.oneboxRef.docker.getContainerHostPort(platformService.containerId, 27017);
if (!hostPort) {
throw new Error('Could not get MongoDB container host port');
}
logger.info(`Deprovisioning MongoDB database '${resource.resourceName}'...`); logger.info(`Deprovisioning MongoDB database '${resource.resourceName}'...`);
const { MongoClient } = await import('npm:mongodb@6'); const mongoScript = `
const adminUri = `mongodb://${adminCreds.username}:${adminCreds.password}@127.0.0.1:${hostPort}/?authSource=admin`; db = db.getSiblingDB('${resource.resourceName}');
try { db.dropUser('${credentials.username}'); } catch(e) { print('User drop failed: ' + e); }
db.dropDatabase();
print('DEPROVISION_SUCCESS');
`;
const client = new MongoClient(adminUri); const result = await this.oneboxRef.docker.execInContainer(
await client.connect(); platformService.containerId,
[
'mongo',
'--username', adminCreds.username,
'--password', escapedAdminPassword,
'--authenticationDatabase', 'admin',
'--quiet',
'--eval', mongoScript,
]
);
try { if (result.exitCode !== 0) {
const db = client.db(resource.resourceName); logger.warn(`MongoDB deprovision returned exit code ${result.exitCode}: ${result.stderr.substring(0, 200)}`);
// Drop the user
try {
await db.command({ dropUser: credentials.username });
logger.info(`Dropped MongoDB user '${credentials.username}'`);
} catch (e) {
logger.warn(`Could not drop MongoDB user: ${getErrorMessage(e)}`);
}
// Drop the database
await db.dropDatabase();
logger.success(`MongoDB database '${resource.resourceName}' dropped`);
} finally {
await client.close();
} }
logger.success(`MongoDB database '${resource.resourceName}' dropped`);
} }
} }

View File

@@ -165,5 +165,42 @@ export class PlatformHandler {
}, },
), ),
); );
// Get platform service logs
this.typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetPlatformServiceLogs>(
'getPlatformServiceLogs',
async (dataArg) => {
await requireValidIdentity(this.opsServerRef.adminHandler, dataArg);
const service = this.opsServerRef.oneboxRef.database.getPlatformServiceByType(dataArg.serviceType);
if (!service || !service.containerId) {
throw new plugins.typedrequest.TypedResponseError('Platform service has no container');
}
const tail = dataArg.tail || 100;
const rawLogs = await this.opsServerRef.oneboxRef.docker.getContainerLogs(service.containerId, tail);
// Parse raw log output into structured entries
const logLines = (rawLogs.stdout + rawLogs.stderr)
.split('\n')
.filter((line: string) => line.trim());
const logs = logLines.map((line: string, index: number) => {
const isError = line.toLowerCase().includes('error') || line.toLowerCase().includes('fatal');
const isWarn = line.toLowerCase().includes('warn');
return {
id: index,
serviceId: 0,
timestamp: Date.now(),
message: line,
level: (isError ? 'error' : isWarn ? 'warn' : 'info') as 'info' | 'warn' | 'error' | 'debug',
source: 'stdout' as const,
};
});
return { logs };
},
),
);
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -69,3 +69,18 @@ export interface IReq_GetPlatformServiceStats extends plugins.typedrequestInterf
stats: data.IContainerStats; stats: data.IContainerStats;
}; };
} }
export interface IReq_GetPlatformServiceLogs extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_GetPlatformServiceLogs
> {
method: 'getPlatformServiceLogs';
request: {
identity: data.IIdentity;
serviceType: data.TPlatformServiceType;
tail?: number;
};
response: {
logs: data.ILogEntry[];
};
}

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/onebox', name: '@serve.zone/onebox',
version: '1.17.1', version: '1.18.2',
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers' description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
} }

View File

@@ -27,6 +27,7 @@ export interface IServicesState {
platformServices: interfaces.data.IPlatformService[]; platformServices: interfaces.data.IPlatformService[];
currentPlatformService: interfaces.data.IPlatformService | null; currentPlatformService: interfaces.data.IPlatformService | null;
currentPlatformServiceStats: interfaces.data.IContainerStats | null; currentPlatformServiceStats: interfaces.data.IContainerStats | null;
currentPlatformServiceLogs: interfaces.data.ILogEntry[];
} }
export interface INetworkState { export interface INetworkState {
@@ -90,6 +91,7 @@ export const servicesStatePart = await appState.getStatePart<IServicesState>(
platformServices: [], platformServices: [],
currentPlatformService: null, currentPlatformService: null,
currentPlatformServiceStats: null, currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
}, },
'soft', 'soft',
); );
@@ -497,6 +499,27 @@ export const fetchPlatformServiceStatsAction = servicesStatePart.createAction<{
} }
}); });
export const fetchPlatformServiceLogsAction = servicesStatePart.createAction<{
serviceType: interfaces.data.TPlatformServiceType;
tail?: number;
}>(async (statePartArg, dataArg) => {
const context = getActionContext();
try {
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
interfaces.requests.IReq_GetPlatformServiceLogs
>('/typedrequest', 'getPlatformServiceLogs');
const response = await typedRequest.fire({
identity: context.identity!,
serviceType: dataArg.serviceType,
tail: dataArg.tail || 100,
});
return { ...statePartArg.getState(), currentPlatformServiceLogs: response.logs };
} catch (err) {
console.error('Failed to fetch platform service logs:', err);
return { ...statePartArg.getState(), currentPlatformServiceLogs: [] };
}
});
// ============================================================================ // ============================================================================
// Network Actions // Network Actions
// ============================================================================ // ============================================================================

View File

@@ -37,15 +37,15 @@ export class ObAppShell extends DeesElement {
accessor loginError: string = ''; accessor loginError: string = '';
private viewTabs = [ private viewTabs = [
{ name: 'Dashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() }, { name: 'Dashboard', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
{ name: 'Services', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() }, { name: 'Services', iconName: 'lucide:boxes', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
{ name: 'Network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() }, { name: 'Network', iconName: 'lucide:network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
{ name: 'Registries', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() }, { name: 'Registries', iconName: 'lucide:package', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
{ name: 'Tokens', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() }, { name: 'Tokens', iconName: 'lucide:key', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
{ name: 'Settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() }, { name: 'Settings', iconName: 'lucide:settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
]; ];
private resolvedViewTabs: Array<{ name: string; element: any }> = []; private resolvedViewTabs: Array<{ name: string; iconName?: string; element: any }> = [];
constructor() { constructor() {
super(); super();
@@ -104,6 +104,7 @@ export class ObAppShell extends DeesElement {
this.resolvedViewTabs = await Promise.all( this.resolvedViewTabs = await Promise.all(
this.viewTabs.map(async (tab) => ({ this.viewTabs.map(async (tab) => ({
name: tab.name, name: tab.name,
iconName: tab.iconName,
element: await tab.element, element: await tab.element,
})), })),
); );

View File

@@ -25,6 +25,7 @@ export class ObViewDashboard extends DeesElement {
platformServices: [], platformServices: [],
currentPlatformService: null, currentPlatformService: null,
currentPlatformServiceStats: null, currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
}; };
@state() @state()

View File

@@ -108,6 +108,7 @@ export class ObViewServices extends DeesElement {
platformServices: [], platformServices: [],
currentPlatformService: null, currentPlatformService: null,
currentPlatformServiceStats: null, currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
}; };
@state() @state()
@@ -357,10 +358,10 @@ export class ObViewServices extends DeesElement {
private navigateToPlatformDetail(type: string): void { private navigateToPlatformDetail(type: string): void {
this.selectedPlatformType = type; this.selectedPlatformType = type;
// Fetch stats for this platform service // Fetch stats and logs for this platform service
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, { const serviceType = type as interfaces.data.TPlatformServiceType;
serviceType: type as interfaces.data.TPlatformServiceType, appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, { serviceType });
}); appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceLogsAction, { serviceType });
this.currentView = 'platform-detail'; this.currentView = 'platform-detail';
} }
@@ -398,7 +399,11 @@ export class ObViewServices extends DeesElement {
metrics, metrics,
} }
: null} : null}
.logs=${[]} .logs=${this.servicesState.currentPlatformServiceLogs.map((log) => ({
timestamp: new Date(log.timestamp).toLocaleString(),
level: log.level,
message: log.message,
}))}
@back=${() => { @back=${() => {
this.currentView = 'list'; this.currentView = 'list';
}} }}