Compare commits

...

27 Commits

Author SHA1 Message Date
3108408133 v1.20.0
Some checks failed
Publish to npm / npm-publish (push) Failing after 7s
CI / Type Check & Lint (push) Failing after 39s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 2m52s
Release / build-and-release (push) Successful in 6m2s
2026-03-17 23:39:25 +00:00
6defdb4431 feat(ops-dashboard): stream user service logs to the ops dashboard and resolve service containers for Docker log streaming 2026-03-17 23:39:24 +00:00
f63be883ce v1.19.12
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Build All Platforms (push) Failing after 13m15s
CI / Build Test (Current Platform) (push) Failing after 13m17s
CI / Type Check & Lint (push) Failing after 13m19s
Release / build-and-release (push) Successful in 6m6s
2026-03-17 12:52:34 +00:00
87844bbb8e fix(repo): no changes to commit 2026-03-17 12:52:34 +00:00
02b7cda2be v1.19.11
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 1m0s
CI / Build All Platforms (push) Successful in 1m57s
Release / build-and-release (push) Successful in 5m51s
2026-03-17 10:40:40 +00:00
3b8f95e8e1 fix(repo): no changes to commit 2026-03-17 10:40:40 +00:00
ee774e3f41 v1.19.10
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 38s
CI / Build Test (Current Platform) (push) Successful in 1m6s
CI / Build All Platforms (push) Successful in 2m17s
Release / build-and-release (push) Successful in 7m46s
2026-03-17 02:09:50 +00:00
6d93dfa459 fix(repo): no changes to commit 2026-03-17 02:09:50 +00:00
ac394cfafc v1.19.9
Some checks failed
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 1m2s
CI / Build All Platforms (push) Successful in 2m0s
Release / build-and-release (push) Successful in 2m51s
2026-03-17 01:53:38 +00:00
97e9f232fa fix(repo): no changes to commit 2026-03-17 01:53:38 +00:00
3dcb6a38e5 v1.19.8
Some checks failed
CI / Build All Platforms (push) Failing after 9s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 28s
CI / Build Test (Current Platform) (push) Successful in 59s
Release / build-and-release (push) Failing after 1m24s
2026-03-17 01:37:18 +00:00
ca33970e9a fix(repo): no changes to commit 2026-03-17 01:37:18 +00:00
cd34b98a25 v1.19.7
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 33s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 1m52s
Release / build-and-release (push) Successful in 3m50s
2026-03-17 01:03:14 +00:00
a089e5bedb fix(repo): no changes to commit 2026-03-17 01:03:14 +00:00
9786ff62f0 v1.19.6
Some checks failed
Publish to npm / npm-publish (push) Failing after 9s
CI / Type Check & Lint (push) Failing after 31s
CI / Build Test (Current Platform) (push) Successful in 1m6s
CI / Build All Platforms (push) Successful in 2m15s
Release / build-and-release (push) Failing after 3m30s
2026-03-17 00:45:56 +00:00
4a5abc4a0a fix(repository): no changes to commit 2026-03-17 00:45:55 +00:00
893a532758 v1.19.5
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
Release / build-and-release (push) Failing after 3m34s
2026-03-17 00:45:37 +00:00
7ea286c0a9 fix(repo): no changes to commit 2026-03-17 00:45:37 +00:00
f94f47e313 v1.19.4
Some checks failed
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 32s
CI / Build Test (Current Platform) (push) Successful in 1m7s
CI / Build All Platforms (push) Successful in 2m8s
Release / build-and-release (push) Successful in 3m38s
2026-03-17 00:01:08 +00:00
b1a46f8757 fix(repository): no changes to commit 2026-03-17 00:01:08 +00:00
56c71226e5 v1.19.3
Some checks failed
CI / Build All Platforms (push) Failing after 3s
Publish to npm / npm-publish (push) Failing after 8s
CI / Type Check & Lint (push) Failing after 36s
CI / Build Test (Current Platform) (push) Successful in 1m3s
Release / build-and-release (push) Successful in 2m48s
2026-03-16 20:06:11 +00:00
f53109a01e fix(repo): no changes to commit 2026-03-16 20:06:11 +00:00
bcb2473cc5 v1.19.2
Some checks failed
Publish to npm / npm-publish (push) Failing after 10s
CI / Type Check & Lint (push) Failing after 34s
CI / Build Test (Current Platform) (push) Successful in 1m10s
CI / Build All Platforms (push) Successful in 2m34s
Release / build-and-release (push) Failing after 4m2s
2026-03-16 20:00:10 +00:00
689dcf295b fix(docs): remove outdated UI screenshot assets from project documentation 2026-03-16 20:00:10 +00:00
c1e14e9fc7 v1.19.1
Some checks failed
CI / Type Check & Lint (push) Failing after 30s
CI / Build Test (Current Platform) (push) Successful in 1m3s
CI / Build All Platforms (push) Successful in 2m5s
2026-03-16 19:55:27 +00:00
d5fd57e2c3 fix(dashboard): add updated dashboard screenshots for refresh and resource usage states 2026-03-16 19:55:27 +00:00
079e6a64a9 fix(dashboard): add aggregated resource usage stats to the dashboard
Some checks failed
Publish to npm / npm-publish (push) Failing after 4s
CI / Type Check & Lint (push) Failing after 29s
CI / Build Test (Current Platform) (push) Successful in 1m1s
Release / build-and-release (push) Failing after 1m43s
CI / Build All Platforms (push) Successful in 2m8s
- Aggregate CPU, memory, and network stats across all running containers
- Extend ISystemStatus.docker with resource usage fields
- Fix getContainerStats Swarm service ID fallback
- Wire dashboard resource usage card to real backend data
2026-03-16 16:47:05 +00:00
20 changed files with 304 additions and 49 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,5 +1,74 @@
# Changelog
## 2026-03-17 - 1.20.0 - feat(ops-dashboard)
stream user service logs to the ops dashboard and resolve service containers for Docker log streaming
- add typed socket support for pushing live user service log entries to the web app
- extend platform log streaming to include running user services with separate dashboard handlers
- fall back from direct container lookup to service-to-container resolution when streaming Docker logs
- update log parsing to preserve timestamps and infer log levels for service log entries
- bump @serve.zone/catalog to ^2.7.0
## 2026-03-17 - 1.19.12 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.11 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.10 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.9 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.8 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.7 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.6 - fix(repository)
no changes to commit
## 2026-03-17 - 1.19.5 - fix(repo)
no changes to commit
## 2026-03-17 - 1.19.4 - fix(repository)
no changes to commit
## 2026-03-16 - 1.19.3 - fix(repo)
no changes to commit
## 2026-03-16 - 1.19.2 - fix(docs)
remove outdated UI screenshot assets from project documentation
- Deletes multiple PNG screenshots that documented previous dashboard, service form, and hello-world states.
- Reduces repository clutter by removing obsolete image assets no longer needed in docs.
## 2026-03-16 - 1.19.1 - fix(dashboard)
add updated dashboard screenshots for refresh and resource usage states
- Adds new dashboard screenshots covering post-refresh, resource usage, and populated data views.
- Updates visual assets to document current dashboard behavior and UI states.
## 2026-03-16 - 1.19.1 - fix(dashboard)
add aggregated resource usage stats to the dashboard
- Aggregate CPU, memory, and network stats across all running user and platform service containers in getSystemStatus
- Extend ISystemStatus.docker interface with cpuUsage, memoryUsage, memoryTotal, networkIn, networkOut fields
- Fix getContainerStats to properly handle Swarm service IDs by catching exceptions and falling back to label-based container lookup
- Wire dashboard resource usage card to display real aggregated data from the backend
## 2026-03-16 - 1.19.0 - feat(opsserver,web)
add real-time platform service log streaming to the dashboard

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/onebox",
"version": "1.19.0",
"version": "1.20.0",
"description": "Self-hosted container platform with automatic SSL and DNS - a mini Heroku for single servers",
"main": "mod.ts",
"type": "module",
@@ -58,7 +58,7 @@
"@api.global/typedsocket": "^4.1.2",
"@design.estate/dees-catalog": "^3.43.3",
"@design.estate/dees-element": "^2.1.6",
"@serve.zone/catalog": "^2.6.2"
"@serve.zone/catalog": "^2.7.0"
},
"devDependencies": {
"@git.zone/tsbundle": "^2.9.0",

10
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^2.1.6
version: 2.2.3
'@serve.zone/catalog':
specifier: ^2.6.2
version: 2.6.2(@tiptap/pm@2.27.2)
specifier: ^2.7.0
version: 2.7.0(@tiptap/pm@2.27.2)
devDependencies:
'@git.zone/tsbundle':
specifier: ^2.9.0
@@ -839,8 +839,8 @@ packages:
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
'@serve.zone/catalog@2.6.2':
resolution: {integrity: sha512-1XPdgkqjx80r3mjE03QOex0r48jz2SzQ8lwz/VBvPtwgJYH0DO5TBuMSgT56YeQ1c/e2vVpqdXIicbcJoreBYw==}
'@serve.zone/catalog@2.7.0':
resolution: {integrity: sha512-BSfLi9BZE5wvu5Dxh0p/mM9bE+9lf35PGHRZ1Cw/+YpWxOfIFPTZKkBz2OUn3yctWw+V7l1VBBYuLX1bVCKFfA==}
'@tempfix/idb@8.0.3':
resolution: {integrity: sha512-hPJQKO7+oAIY+pDNImrZ9QAINbz9KmwT+yO4iRVwdPanok2YKpaUxdJzIvCUwY0YgAawlvYdffbLvRLV5hbs2g==}
@@ -3477,7 +3477,7 @@ snapshots:
'@sec-ant/readable-stream@0.4.1': {}
'@serve.zone/catalog@2.6.2(@tiptap/pm@2.27.2)':
'@serve.zone/catalog@2.7.0(@tiptap/pm@2.27.2)':
dependencies:
'@design.estate/dees-catalog': 3.48.5(@tiptap/pm@2.27.2)
'@design.estate/dees-domtools': 2.5.1

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

View File

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

View File

@@ -596,18 +596,26 @@ export class OneboxDockerManager {
async getContainerStats(containerID: string): Promise<IContainerStats | null> {
try {
// Try to get container directly first
let container = await this.dockerClient!.getContainerById(containerID);
let container: any = null;
try {
container = await this.dockerClient!.getContainerById(containerID);
} catch {
// Container not found by ID — might be a Swarm service ID
}
// 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) {
container = await this.dockerClient!.getContainerById(serviceContainerId);
try {
container = await this.dockerClient!.getContainerById(serviceContainerId);
} catch {
// Service container also not found
}
}
}
if (!container) {
// Container/service not found
return null;
}
@@ -1011,7 +1019,23 @@ export class OneboxDockerManager {
callback: (line: string, isError: boolean) => void
): Promise<void> {
try {
const container = await this.dockerClient!.getContainerById(containerID);
let container: any = null;
try {
container = await this.dockerClient!.getContainerById(containerID);
} catch {
// Not a direct container ID — try Swarm service lookup
}
if (!container) {
const serviceContainerId = await this.getContainerIdForService(containerID);
if (serviceContainerId) {
try {
container = await this.dockerClient!.getContainerById(serviceContainerId);
} catch {
// Service container also not found
}
}
}
if (!container) {
throw new Error(`Container not found: ${containerID}`);

View File

@@ -291,10 +291,65 @@ export class Onebox {
// Sort expiring domains by days remaining (ascending)
expiringDomains.sort((a, b) => a.daysRemaining - b.daysRemaining);
// Aggregate resource usage across all running service containers
let totalCpu = 0;
let totalMemoryUsed = 0;
let totalMemoryLimit = 0;
let totalNetworkIn = 0;
let totalNetworkOut = 0;
if (dockerRunning) {
const allServices = this.services.listServices();
const runningUserServices = allServices.filter((s) => s.status === 'running' && s.containerID);
logger.debug(`Resource stats: ${runningUserServices.length} running user services`);
const statsPromises = runningUserServices
.map((s) => {
logger.debug(`Fetching stats for user service: ${s.name} (${s.containerID})`);
return this.docker.getContainerStats(s.containerID!).catch((err) => {
logger.debug(`Stats failed for ${s.name}: ${(err as Error).message}`);
return null;
});
});
// Also get stats for platform service containers
const allPlatformServices = this.platformServices.getAllPlatformServices();
const runningPlatformServices = allPlatformServices.filter((s) => s.status === 'running' && s.containerId);
logger.debug(`Resource stats: ${runningPlatformServices.length} running platform services`);
const platformStatsPromises = runningPlatformServices
.map((s) => {
logger.debug(`Fetching stats for platform service: ${s.type} (${s.containerId})`);
return this.docker.getContainerStats(s.containerId!).catch((err) => {
logger.debug(`Stats failed for ${s.type}: ${(err as Error).message}`);
return null;
});
});
const allStats = await Promise.all([...statsPromises, ...platformStatsPromises]);
let successCount = 0;
for (const stats of allStats) {
if (stats) {
successCount++;
totalCpu += stats.cpuPercent;
totalMemoryUsed += stats.memoryUsed;
totalMemoryLimit = Math.max(totalMemoryLimit, stats.memoryLimit);
totalNetworkIn += stats.networkRx;
totalNetworkOut += stats.networkTx;
}
}
logger.debug(`Resource stats: ${successCount}/${allStats.length} containers returned stats. CPU: ${totalCpu}, Mem: ${totalMemoryUsed}`);
}
return {
docker: {
running: dockerRunning,
version: dockerRunning ? await this.docker.getDockerVersion() : null,
cpuUsage: Math.round(totalCpu * 10) / 10,
memoryUsage: totalMemoryUsed,
memoryTotal: totalMemoryLimit,
networkIn: totalNetworkIn,
networkOut: totalNetworkOut,
},
reverseProxy: proxyStatus,
dns: {

View File

@@ -15,30 +15,54 @@ export class PlatformHandler {
}
/**
* Start streaming logs from all running platform service containers
* Start streaming logs from all running containers (platform + user services)
* 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) {
// Stream platform service containers
const platformServices = this.opsServerRef.oneboxRef.database.getAllPlatformServices();
for (const service of platformServices) {
if (service.status !== 'running' || !service.containerId) continue;
if (this.activeLogStreams.has(service.type)) continue;
const key = `platform:${service.type}`;
if (this.activeLogStreams.has(key)) continue;
this.activeLogStreams.set(service.type, true);
this.activeLogStreams.set(key, 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);
this.pushPlatformLogToClients(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);
this.activeLogStreams.delete(key);
}
}
// Stream user service containers
const userServices = this.opsServerRef.oneboxRef.services.listServices();
for (const service of userServices) {
if (service.status !== 'running' || !service.containerID) continue;
const key = `service:${service.name}`;
if (this.activeLogStreams.has(key)) continue;
this.activeLogStreams.set(key, true);
logger.info(`Starting log stream for user service: ${service.name}`);
try {
await this.opsServerRef.oneboxRef.docker.streamContainerLogs(
service.containerID,
(line: string, isError: boolean) => {
this.pushServiceLogToClients(service.name, line, isError);
}
);
} catch (err) {
logger.warn(`Log stream failed for ${service.name}: ${(err as Error).message}`);
this.activeLogStreams.delete(key);
}
}
};
@@ -49,15 +73,7 @@ export class PlatformHandler {
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
private parseLogLine(line: string, isError: boolean): { timestamp: string; level: string; message: string } {
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;
@@ -67,21 +83,51 @@ export class PlatformHandler {
: msgLower.includes('warn')
? 'warn'
: 'info';
return { timestamp, level, message };
}
private pushPlatformLogToClients(
serviceType: interfaces.data.TPlatformServiceType,
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
const entry = this.parseLogLine(line, isError);
// 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
).fire({ serviceType, entry }).catch(() => {});
}
})
.catch(() => {}); // no connections, ignore
.catch(() => {});
}
private pushServiceLogToClients(
serviceName: string,
line: string,
isError: boolean,
): void {
const typedsocket = (this.opsServerRef.server as any)?.typedserver?.typedsocket;
if (!typedsocket) return;
const entry = this.parseLogLine(line, isError);
typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard')
.then((connections: any[]) => {
for (const conn of connections) {
typedsocket.createTypedRequest<interfaces.requests.IReq_PushServiceLog>(
'pushServiceLog',
conn,
).fire({ serviceName, entry }).catch(() => {});
}
})
.catch(() => {});
}
private registerHandlers(): void {

File diff suppressed because one or more lines are too long

View File

@@ -8,6 +8,11 @@ export interface ISystemStatus {
docker: {
running: boolean;
version: unknown;
cpuUsage: number;
memoryUsage: number;
memoryTotal: number;
networkIn: number;
networkOut: number;
};
reverseProxy: {
http: { running: boolean; port: number };

View File

@@ -212,3 +212,19 @@ export interface IReq_GetServiceBackupSchedules extends plugins.typedrequestInte
schedules: data.IBackupSchedule[];
};
}
export interface IReq_PushServiceLog extends plugins.typedrequestInterfaces.implementsTR<
plugins.typedrequestInterfaces.ITypedRequest,
IReq_PushServiceLog
> {
method: 'pushServiceLog';
request: {
serviceName: string;
entry: {
timestamp: string;
level: string;
message: string;
};
};
response: {};
}

View File

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

View File

@@ -997,6 +997,37 @@ socketRouter.addTypedHandler(
),
);
// Handle server-pushed user service log entries
socketRouter.addTypedHandler(
new plugins.domtools.plugins.typedrequest.TypedHandler<interfaces.requests.IReq_PushServiceLog>(
'pushServiceLog',
async (dataArg) => {
const state = servicesStatePart.getState();
// Only append if we're currently viewing this service
if (!state.currentService || state.currentService.name !== dataArg.serviceName) {
return {};
}
const entry: interfaces.data.ILogEntry = {
id: state.currentServiceLogs.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.currentServiceLogs, entry];
if (updated.length > 2000) {
updated.splice(0, updated.length - 2000);
}
servicesStatePart.setState({
...state,
currentServiceLogs: updated,
});
return {};
},
),
);
async function connectSocket() {
if (socketClient) return;
try {

View File

@@ -110,8 +110,8 @@ export class ObViewDashboard extends DeesElement {
cpu: status?.docker?.cpuUsage || 0,
memoryUsed: status?.docker?.memoryUsage || 0,
memoryTotal: status?.docker?.memoryTotal || 0,
networkIn: 0,
networkOut: 0,
networkIn: status?.docker?.networkIn || 0,
networkOut: status?.docker?.networkOut || 0,
topConsumers: [],
},
platformServices: platformServices.map((ps) => ({

View File

@@ -76,20 +76,29 @@ function toServiceStats(stats: interfaces.data.IContainerStats) {
};
}
function parseLogs(logs: any): Array<{ timestamp: string; message: string }> {
function parseLogs(logs: any): Array<{ timestamp: string; message: string; level?: string }> {
if (Array.isArray(logs)) {
return logs.map((entry: any) => ({
timestamp: entry.timestamp ? String(entry.timestamp) : '',
message: entry.message || String(entry),
}));
return logs.map((entry: any) => {
const ts = entry.timestamp
? (typeof entry.timestamp === 'number' ? new Date(entry.timestamp).toISOString() : String(entry.timestamp))
: new Date().toISOString();
const message = entry.message || String(entry);
const level = entry.level || 'info';
return { timestamp: ts, message, level };
});
}
if (typeof logs === 'string' && logs.trim()) {
return logs.split('\n').filter((line: string) => line.trim()).map((line: string) => {
const match = line.match(/^(\d{4}-\d{2}-\d{2}T[\d:.]+Z?)\s+(.*)/);
if (match) {
return { timestamp: match[1], message: match[2] };
}
return { timestamp: '', message: line };
const timestamp = match ? match[1] : new Date().toISOString();
const message = match ? match[2] : line;
const msgLower = message.toLowerCase();
const level = msgLower.includes('error') || msgLower.includes('fatal')
? 'error'
: msgLower.includes('warn')
? 'warn'
: 'info';
return { timestamp, message, level };
});
}
return [];