feat(systemd): unit overview + journal viewer pages

- Add journalctl_recent command and scrum4me-web to whitelist in commands.yml.example
- Add SYSTEMD_UNITS env var to .env.example
- lib/parse-systemd.ts: parse activeState, subState, uptime, description
- /app/systemd: server page reading SYSTEMD_UNITS, client list with 10s polling and status badges
- /app/systemd/[unit]: server detail page, client component showing systemctl status + last 100 journal lines (polling 10s)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scrum4Me Agent 2026-05-13 17:41:54 +02:00
parent 9e08a7c31f
commit c12e36e0a4
7 changed files with 523 additions and 1 deletions

34
lib/parse-systemd.ts Normal file
View file

@ -0,0 +1,34 @@
export type ActiveState = 'active' | 'inactive' | 'failed' | 'activating' | 'deactivating' | 'unknown'
export interface UnitStatus {
activeState: ActiveState
subState: string
uptime: string
description: string
}
const KNOWN_STATES = new Set(['active', 'inactive', 'failed', 'activating', 'deactivating'])
export function parseSystemctlStatus(output: string, unitName: string): UnitStatus {
let activeState: ActiveState = 'unknown'
let subState = ''
let uptime = ''
let description = unitName
for (const line of output.split('\n')) {
// Header line: "● scrum4me-web.service - Description text"
const headerMatch = line.match(/^\s*[●○×◉]\s+\S+\s+-\s+(.+)/)
if (headerMatch) description = headerMatch[1].trim()
// Active line: " Active: active (running) since Tue 2025-01-13...; 2h 30min ago"
const activeMatch = line.match(/\bActive:\s+(\w+)(?:\s+\(([^)]+)\))?(?:.*?;\s+(.+?ago))?/)
if (activeMatch) {
const state = activeMatch[1].toLowerCase()
activeState = KNOWN_STATES.has(state) ? (state as ActiveState) : 'unknown'
subState = activeMatch[2] ?? ''
uptime = activeMatch[3] ?? ''
}
}
return { activeState, subState, uptime, description }
}