feat(platform-services): add platform service log retrieval and display in the services UI
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
BIN
sidebar-icons.png
Normal file
BIN
sidebar-icons.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/onebox',
|
||||
version: '1.17.4',
|
||||
version: '1.18.0',
|
||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||
}
|
||||
|
||||
@@ -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
@@ -69,3 +69,18 @@ export interface IReq_GetPlatformServiceStats extends plugins.typedrequestInterf
|
||||
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[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/onebox',
|
||||
version: '1.17.4',
|
||||
version: '1.18.0',
|
||||
description: 'Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers'
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface IServicesState {
|
||||
platformServices: interfaces.data.IPlatformService[];
|
||||
currentPlatformService: interfaces.data.IPlatformService | null;
|
||||
currentPlatformServiceStats: interfaces.data.IContainerStats | null;
|
||||
currentPlatformServiceLogs: interfaces.data.ILogEntry[];
|
||||
}
|
||||
|
||||
export interface INetworkState {
|
||||
@@ -90,6 +91,7 @@ export const servicesStatePart = await appState.getStatePart<IServicesState>(
|
||||
platformServices: [],
|
||||
currentPlatformService: null,
|
||||
currentPlatformServiceStats: null,
|
||||
currentPlatformServiceLogs: [],
|
||||
},
|
||||
'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
|
||||
// ============================================================================
|
||||
|
||||
@@ -37,15 +37,15 @@ export class ObAppShell extends DeesElement {
|
||||
accessor loginError: string = '';
|
||||
|
||||
private viewTabs = [
|
||||
{ name: 'Dashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
|
||||
{ name: 'Services', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
|
||||
{ name: 'Network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
|
||||
{ name: 'Registries', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
|
||||
{ name: 'Tokens', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
|
||||
{ name: 'Settings', element: (async () => (await import('./ob-view-settings.js')).ObViewSettings)() },
|
||||
{ name: 'Dashboard', iconName: 'lucide:layoutDashboard', element: (async () => (await import('./ob-view-dashboard.js')).ObViewDashboard)() },
|
||||
{ name: 'Services', iconName: 'lucide:boxes', element: (async () => (await import('./ob-view-services.js')).ObViewServices)() },
|
||||
{ name: 'Network', iconName: 'lucide:network', element: (async () => (await import('./ob-view-network.js')).ObViewNetwork)() },
|
||||
{ name: 'Registries', iconName: 'lucide:package', element: (async () => (await import('./ob-view-registries.js')).ObViewRegistries)() },
|
||||
{ name: 'Tokens', iconName: 'lucide:key', element: (async () => (await import('./ob-view-tokens.js')).ObViewTokens)() },
|
||||
{ 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() {
|
||||
super();
|
||||
@@ -104,6 +104,7 @@ export class ObAppShell extends DeesElement {
|
||||
this.resolvedViewTabs = await Promise.all(
|
||||
this.viewTabs.map(async (tab) => ({
|
||||
name: tab.name,
|
||||
iconName: tab.iconName,
|
||||
element: await tab.element,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ export class ObViewDashboard extends DeesElement {
|
||||
platformServices: [],
|
||||
currentPlatformService: null,
|
||||
currentPlatformServiceStats: null,
|
||||
currentPlatformServiceLogs: [],
|
||||
};
|
||||
|
||||
@state()
|
||||
|
||||
@@ -108,6 +108,7 @@ export class ObViewServices extends DeesElement {
|
||||
platformServices: [],
|
||||
currentPlatformService: null,
|
||||
currentPlatformServiceStats: null,
|
||||
currentPlatformServiceLogs: [],
|
||||
};
|
||||
|
||||
@state()
|
||||
@@ -357,10 +358,10 @@ export class ObViewServices extends DeesElement {
|
||||
|
||||
private navigateToPlatformDetail(type: string): void {
|
||||
this.selectedPlatformType = type;
|
||||
// Fetch stats for this platform service
|
||||
appstate.servicesStatePart.dispatchAction(appstate.fetchPlatformServiceStatsAction, {
|
||||
serviceType: type as interfaces.data.TPlatformServiceType,
|
||||
});
|
||||
// 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 });
|
||||
this.currentView = 'platform-detail';
|
||||
}
|
||||
|
||||
@@ -398,7 +399,11 @@ export class ObViewServices extends DeesElement {
|
||||
metrics,
|
||||
}
|
||||
: null}
|
||||
.logs=${[]}
|
||||
.logs=${this.servicesState.currentPlatformServiceLogs.map((log) => ({
|
||||
timestamp: new Date(log.timestamp).toLocaleString(),
|
||||
level: log.level,
|
||||
message: log.message,
|
||||
}))}
|
||||
@back=${() => {
|
||||
this.currentView = 'list';
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user