Compare commits

...

15 Commits

Author SHA1 Message Date
a04cf053db v1.19.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 11s
CI / Type Check & Lint (push) Failing after 30s
CI / Build Test (Current Platform) (push) Successful in 58s
CI / Build All Platforms (push) Successful in 2m21s
Release / build-and-release (push) Successful in 4m11s
2026-03-16 16:19:39 +00:00
ec0e377ccb feat(opsserver,web): add real-time platform service log streaming to the dashboard 2026-03-16 16:19:39 +00:00
3b3d0433cb fix(platform-services): fix detail view navigation and log display
Some checks failed
CI / Build All Platforms (push) Failing after 4s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m6s
Release / build-and-release (push) Successful in 3m21s
- Add back button for returning to services list
- Fix DOM lifecycle when switching between platform services
- Fix timestamp format for dees-chart-log compatibility
- Clear previous stats/logs state before fetching new data
2026-03-16 14:48:46 +00:00
5f876449ca v1.18.4
Some checks failed
CI / Build All Platforms (push) Failing after 6s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 24s
CI / Build Test (Current Platform) (push) Successful in 54s
Release / build-and-release (push) Successful in 2m19s
2026-03-16 14:35:45 +00:00
8e781c7f9d fix(repo): no changes to commit 2026-03-16 14:35:45 +00:00
a3eefbe92c v1.18.3
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m1s
CI / Build All Platforms (push) Successful in 2m3s
Release / build-and-release (push) Successful in 3m2s
2026-03-16 14:22:37 +00:00
41679427c6 fix(deps): bump @serve.zone/catalog to ^2.6.1 2026-03-16 14:22:37 +00:00
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
18 changed files with 390 additions and 42 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,5 +1,51 @@
# Changelog # Changelog
## 2026-03-16 - 1.19.0 - feat(opsserver,web)
add real-time platform service log streaming to the dashboard
- stream running platform service container logs from the ops server to connected dashboard clients via TypedSocket
- parse Docker log timestamps and levels for both pushed and fetched platform service log entries
- enhance the platform service detail view with mapped statuses and predefined host, port, version, and config metadata
- add the typedsocket dependency and update the catalog package for dashboard support
## 2026-03-16 - 1.18.5 - fix(platform-services)
fix platform service detail view navigation and log display
- Add back button to platform service detail view for returning to services list
- Fix DOM lifecycle when switching between platform services (destroy and recreate dees-chart-log)
- Fix timestamp format for log entries to use ISO 8601 for dees-chart-log compatibility
- Clear previous stats/logs state before fetching new platform service data
## 2026-03-16 - 1.18.4 - fix(repo)
no changes to commit
## 2026-03-16 - 1.18.3 - fix(deps)
bump @serve.zone/catalog to ^2.6.1
- Updates the @serve.zone/catalog runtime dependency from ^2.6.0 to ^2.6.1.
## 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) ## 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 downgrade the MongoDB service image to 4.4 and use the legacy mongo shell for container operations

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.17.3", "version": "1.19.0",
"exports": "./mod.ts", "exports": "./mod.ts",
"tasks": { "tasks": {
"test": "deno test --allow-all test/", "test": "deno test --allow-all test/",
@@ -25,7 +25,8 @@
"@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6", "@api.global/typedrequest": "npm:@api.global/typedrequest@^3.2.6",
"@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1", "@api.global/typedserver": "npm:@api.global/typedserver@^8.3.1",
"@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0", "@push.rocks/smartguard": "npm:@push.rocks/smartguard@^3.1.0",
"@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1" "@push.rocks/smartjwt": "npm:@push.rocks/smartjwt@^2.2.1",
"@api.global/typedsocket": "npm:@api.global/typedsocket@^4.1.2"
}, },
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [

BIN
hello-world-running.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/onebox", "name": "@serve.zone/onebox",
"version": "1.17.3", "version": "1.19.0",
"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",
@@ -55,9 +55,10 @@
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34", "packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
"dependencies": { "dependencies": {
"@api.global/typedrequest-interfaces": "^3.0.19", "@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^4.1.2",
"@design.estate/dees-catalog": "^3.43.3", "@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6", "@design.estate/dees-element": "^2.1.6",
"@serve.zone/catalog": "^2.6.0" "@serve.zone/catalog": "^2.6.2"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbundle": "^2.9.0", "@git.zone/tsbundle": "^2.9.0",

13
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@api.global/typedrequest-interfaces': '@api.global/typedrequest-interfaces':
specifier: ^3.0.19 specifier: ^3.0.19
version: 3.0.19 version: 3.0.19
'@api.global/typedsocket':
specifier: ^4.1.2
version: 4.1.2(@push.rocks/smartserve@2.0.1)
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^3.43.3 specifier: ^3.43.3
version: 3.48.5(@tiptap/pm@2.27.2) version: 3.48.5(@tiptap/pm@2.27.2)
@@ -18,8 +21,8 @@ importers:
specifier: ^2.1.6 specifier: ^2.1.6
version: 2.2.3 version: 2.2.3
'@serve.zone/catalog': '@serve.zone/catalog':
specifier: ^2.6.0 specifier: ^2.6.2
version: 2.6.0(@tiptap/pm@2.27.2) version: 2.6.2(@tiptap/pm@2.27.2)
devDependencies: devDependencies:
'@git.zone/tsbundle': '@git.zone/tsbundle':
specifier: ^2.9.0 specifier: ^2.9.0
@@ -836,8 +839,8 @@ packages:
'@sec-ant/readable-stream@0.4.1': '@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@serve.zone/catalog@2.6.0': '@serve.zone/catalog@2.6.2':
resolution: {integrity: sha512-Gp91Ed0MMLMPSAZrH2/UimGxU9AaTu5IPUobPD49PSvh3UnUl+HEFiy81kpKJhW16V37N3/vcNgZrn2VI7/LxQ==} resolution: {integrity: sha512-1XPdgkqjx80r3mjE03QOex0r48jz2SzQ8lwz/VBvPtwgJYH0DO5TBuMSgT56YeQ1c/e2vVpqdXIicbcJoreBYw==}
'@tempfix/idb@8.0.3': '@tempfix/idb@8.0.3':
resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==} resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==}
@@ -3474,7 +3477,7 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {} '@sec-ant/readable-stream@0.4.1': {}
'@serve.zone/catalog@2.6.0(@tiptap/pm@2.27.2)': '@serve.zone/catalog@2.6.2(@tiptap/pm@2.27.2)':
dependencies: dependencies:
'@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2) '@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.1 '@design.estate/dees-domtools': 2.5.1

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.3', version: '1.19.0',
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

@@ -6,10 +6,82 @@ import { requireValidIdentity } from '../helpers/guards.ts';
export class PlatformHandler { export class PlatformHandler {
public typedrouter = new plugins.typedrequest.TypedRouter(); public typedrouter = new plugins.typedrequest.TypedRouter();
private activeLogStreams = new Map<string, boolean>();
constructor(private opsServerRef: OpsServer) { constructor(private opsServerRef: OpsServer) {
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter); this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
this.registerHandlers(); this.registerHandlers();
this.startLogStreaming();
}
/**
* Start streaming logs from all running platform service containers
* and push new entries to connected dashboard clients via TypedSocket
*/
private async startLogStreaming(): Promise<void> {
// Poll for running platform services every 10s and start streams for new ones
const checkAndStream = async () => {
const services = this.opsServerRef.oneboxRef.database.getAllPlatformServices();
for (const service of services) {
if (service.status !== 'running' || !service.containerId) continue;
if (this.activeLogStreams.has(service.type)) continue;
this.activeLogStreams.set(service.type, true);
logger.info(`Starting log stream for platform service: ${service.type}`);
try {
await this.opsServerRef.oneboxRef.docker.streamContainerLogs(
service.containerId,
(line: string, isError: boolean) => {
this.pushLogToClients(service.type as interfaces.data.TPlatformServiceType, line, isError);
}
);
} catch (err) {
logger.warn(`Log stream failed for ${service.type}: ${(err as Error).message}`);
this.activeLogStreams.delete(service.type);
}
}
};
// Initial check after a short delay (let services start first)
setTimeout(() => checkAndStream(), 5000);
// Re-check periodically for newly started services
setInterval(() => checkAndStream(), 15000);
}
private pushLogToClients(
serviceType: interfaces.data.TPlatformServiceType,
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
// Parse timestamp from Docker log line
const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
const timestamp = tsMatch ? tsMatch[1] : new Date().toISOString();
const message = tsMatch ? tsMatch[2] : line;
const msgLower = message.toLowerCase();
const level = isError || msgLower.includes('error') || msgLower.includes('fatal')
? 'error'
: msgLower.includes('warn')
? 'warn'
: 'info';
// Find all dashboard clients and push
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => {
for (const conn of connections) {
typedsocket.createTypedRequest<interfaces.requests.IReq_PushPlatformServiceLog>(
'pushPlatformServiceLog',
conn,
).fire({
serviceType,
entry: { timestamp, level, message },
}).catch(() => {}); // fire-and-forget
}
})
.catch(() => {}); // no connections, ignore
} }
private registerHandlers(): void { private registerHandlers(): void {
@@ -165,5 +237,47 @@ 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) => {
// Try to parse Docker timestamp from beginning of line
const tsMatch = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
const timestamp = tsMatch ? new Date(tsMatch[1]).getTime() : Date.now();
const message = tsMatch ? tsMatch[2] : line;
const msgLower = message.toLowerCase();
const isError = msgLower.includes('error') || msgLower.includes('fatal');
const isWarn = msgLower.includes('warn');
return {
id: index,
serviceId: 0,
timestamp,
message,
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,34 @@ 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[];
};
}
export interface IReq_PushPlatformServiceLog extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushPlatformServiceLog
> {
method: 'pushPlatformServiceLog';
request: {
serviceType: data.TPlatformServiceType;
entry: {
timestamp: string;
level: string;
message: string;
};
};
response: {};
}

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/onebox', name: '@serve.zone/onebox',
version: '1.17.3', version: '1.19.0',
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
// ============================================================================ // ============================================================================
@@ -938,3 +961,73 @@ const startAutoRefresh = () => {
uiStatePart.select((s) => s).subscribe(() => startAutoRefresh()); uiStatePart.select((s) => s).subscribe(() => startAutoRefresh());
loginStatePart.select((s) => s).subscribe(() => startAutoRefresh()); loginStatePart.select((s) => s).subscribe(() => startAutoRefresh());
startAutoRefresh(); startAutoRefresh();
// ============================================================================
// TypedSocket — real-time server push (logs, events)
// ============================================================================
let socketClient: InstanceType<typeof plugins.typedsocket.TypedSocket> | null = null;
const socketRouter = new plugins.domtools.plugins.typedrequest.TypedRouter();
// Handle server-pushed platform service log entries
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushPlatformServiceLog>(
'pushPlatformServiceLog',
async (dataArg) => {
const state = servicesStatePart.getState();
const entry: interfaces.data.ILogEntry = {
id: state.currentPlatformServiceLogs.length,
serviceId: 0,
timestamp: new Date(dataArg.entry.timestamp).getTime(),
message: dataArg.entry.message,
level: dataArg.entry.level as 'info' | 'warn' | 'error' | 'debug',
source: 'stdout',
};
const updated = [...state.currentPlatformServiceLogs, entry];
// Cap at 2000 entries
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
servicesStatePart.setState({
...state,
currentPlatformServiceLogs: updated,
});
return {};
},
),
);
async function connectSocket() {
if (socketClient) return;
try {
socketClient = await plugins.typedsocket.TypedSocket.createClient(
socketRouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl(),
);
await socketClient.setTag('role', 'ops_dashboard');
console.log('TypedSocket dashboard connection established');
} catch (err) {
console.error('TypedSocket connection failed:', err);
socketClient = null;
}
}
async function disconnectSocket() {
if (socketClient) {
try {
await socketClient.disconnect();
} catch {
// ignore disconnect errors
}
socketClient = null;
}
}
// Connect socket when logged in, disconnect when logged out
loginStatePart.select((s) => s).subscribe((loginState) => {
if (loginState.isLoggedIn) {
connectSocket();
} else {
disconnectSocket();
}
});

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()
@@ -221,9 +222,20 @@ export class ObViewServices extends DeesElement {
domain: s.domain || null, domain: s.domain || null,
status: mapStatus(s.status), status: mapStatus(s.status),
})); }));
const displayStatus = (status: string) => {
switch (status) {
case 'running': return 'Running';
case 'stopped': return 'Stopped';
case 'starting': return 'Starting...';
case 'stopping': return 'Stopping...';
case 'failed': return 'Failed';
case 'not-deployed': return 'Not Deployed';
default: return status;
}
};
const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({ const mappedPlatformServices = this.servicesState.platformServices.map((ps) => ({
name: ps.displayName, name: ps.displayName,
status: ps.status === 'running' ? `Running` : ps.status, status: displayStatus(ps.status),
running: ps.status === 'running', running: ps.status === 'running',
type: ps.type, type: ps.type,
})); }));
@@ -356,12 +368,26 @@ export class ObViewServices extends DeesElement {
} }
private navigateToPlatformDetail(type: string): void { private navigateToPlatformDetail(type: string): void {
// Reset to list first to force fresh DOM for dees-chart-log
this.currentView = 'list';
this.selectedPlatformType = type; this.selectedPlatformType = type;
// Fetch stats for this platform service
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, { // Clear previous stats/logs before fetching new ones
serviceType: type as interfaces.data.TPlatformServiceType, appstate.servicesStatePart.setState({
...appstate.servicesStatePart.getState(),
currentPlatformServiceStats: null,
currentPlatformServiceLogs: [],
});
// Fetch stats and logs for this platform service
const serviceType = type as interfaces.data.TPlatformServiceType;
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, { serviceType });
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceLogsAction, { serviceType });
// Switch to detail view on next microtask (ensures fresh DOM)
requestAnimationFrame(() => {
this.currentView = 'platform-detail';
}); });
this.currentView = 'platform-detail';
} }
private renderPlatformDetailView(): TemplateResult { private renderPlatformDetailView(): TemplateResult {
@@ -369,36 +395,63 @@ export class ObViewServices extends DeesElement {
(ps) => ps.type === this.selectedPlatformType, (ps) => ps.type === this.selectedPlatformType,
); );
const stats = this.servicesState.currentPlatformServiceStats; const stats = this.servicesState.currentPlatformServiceStats;
const metrics = stats const metrics = {
? { cpu: stats ? Math.round(stats.cpuPercent) : 0,
cpu: Math.round(stats.cpuPercent), memory: stats ? Math.round(stats.memoryPercent) : 0,
memory: Math.round(stats.memoryPercent), storage: 0,
storage: 0, connections: undefined as number | undefined,
connections: 0, };
}
: undefined; // Real service info per platform type
const serviceInfo: Record<string, { host: string; port: number; version: string; config: Record<string, any> }> = {
mongodb: { host: 'onebox-mongodb', port: 27017, version: '4.4', config: { engine: 'WiredTiger', authEnabled: true } },
minio: { host: 'onebox-minio', port: 9000, version: 'latest', config: { consolePort: 9001, region: 'us-east-1' } },
clickhouse: { host: 'onebox-clickhouse', port: 8123, version: 'latest', config: { nativePort: 9000, httpPort: 8123 } },
caddy: { host: 'onebox-caddy', port: 80, version: '2-alpine', config: { httpsPort: 443, adminApi: 2019 } },
};
const info = platformService
? serviceInfo[platformService.type] || { host: 'unknown', port: 0, version: '', config: {} }
: { host: '', port: 0, version: '', config: {} };
// Map backend status to catalog-compatible status
const mapPlatformStatus = (status: string): 'running' | 'stopped' | 'error' => {
switch (status) {
case 'running': return 'running';
case 'failed': return 'error';
case 'starting':
case 'stopping':
case 'stopped':
case 'not-deployed':
default: return 'stopped';
}
};
return html` return html`
<ob-sectionheading>Platform Service</ob-sectionheading> <ob-sectionheading>Platform Service</ob-sectionheading>
<div class="page-actions" style="justify-content: flex-start;">
<button class="deploy-button" style="background: transparent; border: 1px solid var(--ci-shade-2, #27272a); color: inherit;" @click=${() => { this.currentView = 'list'; }}>
&larr; Back to Services
</button>
</div>
<sz-platform-service-detail-view <sz-platform-service-detail-view
.service=${platformService .service=${platformService
? { ? {
id: platformService.type, id: platformService.type,
name: platformService.displayName, name: platformService.displayName,
type: platformService.type, type: platformService.type,
status: platformService.status === 'running' status: mapPlatformStatus(platformService.status),
? 'running' version: info.version,
: platformService.status === 'failed' host: info.host,
? 'error' port: info.port,
: 'stopped', config: info.config,
version: '',
host: 'localhost',
port: 0,
config: {},
metrics, metrics,
} }
: null} : null}
.logs=${[]} .logs=${this.servicesState.currentPlatformServiceLogs.map((log) => ({
timestamp: new Date(log.timestamp).toISOString(),
level: log.level,
message: log.message,
}))}
@back=${() => { @back=${() => {
this.currentView = 'list'; this.currentView = 'list';
}} }}

View File

@@ -5,9 +5,13 @@ import * as deesCatalog from '@design.estate/dees-catalog';
// @serve.zone scope — side-effect import registers all sz-* custom elements // @serve.zone scope — side-effect import registers all sz-* custom elements
import '@serve.zone/catalog'; import '@serve.zone/catalog';
// TypedSocket for real-time server push (logs, events)
import * as typedsocket from '@api.global/typedsocket';
export { export {
deesElement, deesElement,
deesCatalog, deesCatalog,
typedsocket,
}; };
// domtools gives us TypedRequest, smartstate, smartrouter, and other utilities // domtools gives us TypedRequest, smartstate, smartrouter, and other utilities