Add per-node action groups with schedules, runs, and logs.

Let operators manage ordered action groups on each host, run them manually or on a schedule over SSH, and inspect disk-backed JSON run logs from a node detail UI.
This commit is contained in:
2026-07-19 20:01:11 +02:00
parent 50568127c5
commit 32af91fd30
22 changed files with 4337 additions and 32 deletions
+45 -2
View File
@@ -73,6 +73,7 @@ import {
type LogsPageSize,
} from './logsPreferences'
import { LoginPage } from './LoginPage'
import { NodeDetailPanel } from './NodeDetailPanel'
import {
countAuthFailuresToday,
countNodesBySection,
@@ -86,6 +87,7 @@ import {
pathFor,
subscribeToLocation,
type ConfigTabId,
type NodeDetailTabId,
type ResourceSectionId,
type ResourceTabId,
type SectionId,
@@ -2546,8 +2548,21 @@ function ResourceOverviewTab({
return (
<li key={node.id} className="node-list-item">
<div className="node-list-heading">
<strong>{node.name}</strong>
<code className="node-id">{node.id}</code>
<div className="node-list-heading-main">
<strong>{node.name}</strong>
<code className="node-id">{node.id}</code>
</div>
<button
type="button"
className="node-edit"
onClick={() => {
navigateTo(
pathFor(section, 'overview', 'overview', node.id, 'actions'),
)
}}
>
Edit
</button>
</div>
<p className="config-hint">
{node.host_ip} · {node.username} · group {node.group_name} ·{' '}
@@ -2919,10 +2934,14 @@ function ResourceSectionPanel({
section,
activeResourceTab,
onResourceTabChange,
nodeId,
nodeDetailTab,
}: {
section: ResourceSectionId
activeResourceTab: ResourceTabId
onResourceTabChange: (tab: ResourceTabId) => void
nodeId: string | null
nodeDetailTab: NodeDetailTabId
}) {
const [me, setMe] = useState<MeResponse | null>(null)
const [overviewKey, setOverviewKey] = useState(0)
@@ -2955,6 +2974,16 @@ function ResourceSectionPanel({
const canDelete = Boolean(me?.permissions?.includes('nodes.delete'))
const tabs = resourceTabsFor(section)
if (nodeId) {
return (
<NodeDetailPanel
section={section}
nodeId={nodeId}
activeTab={nodeDetailTab}
/>
)
}
return (
<div className="config-panel">
<div
@@ -3743,6 +3772,11 @@ function App() {
const [activeResourceTab, setActiveResourceTab] = useState<ResourceTabId>(
initialLocation.resourceTab,
)
const [activeNodeId, setActiveNodeId] = useState<string | null>(
initialLocation.nodeId,
)
const [activeNodeDetailTab, setActiveNodeDetailTab] =
useState<NodeDetailTabId>(initialLocation.nodeDetailTab)
const [profileDisplayName, setProfileDisplayName] = useState<string | null>(
null,
)
@@ -3764,6 +3798,8 @@ function App() {
setActiveSection(location.section)
setActiveConfigTab(location.configTab)
setActiveResourceTab(location.resourceTab)
setActiveNodeId(location.nodeId)
setActiveNodeDetailTab(location.nodeDetailTab)
})
}, [])
@@ -3916,6 +3952,8 @@ function App() {
return
}
setActiveSection(section)
setActiveNodeId(null)
setActiveNodeDetailTab('overview')
if (section === 'configuration') {
navigateTo(pathFor(section, activeConfigTab))
return
@@ -3932,6 +3970,7 @@ function App() {
function goToConfigTab(tab: ConfigTabId) {
setActiveSection('configuration')
setActiveConfigTab(tab)
setActiveNodeId(null)
navigateTo(pathFor('configuration', tab))
}
@@ -3940,6 +3979,8 @@ function App() {
return
}
setActiveResourceTab(tab)
setActiveNodeId(null)
setActiveNodeDetailTab('overview')
navigateTo(pathFor(activeSection, 'overview', tab))
}
@@ -4150,6 +4191,8 @@ function App() {
section={activeSection}
activeResourceTab={activeResourceTab}
onResourceTabChange={goToResourceTab}
nodeId={activeNodeId}
nodeDetailTab={activeNodeDetailTab}
/>
) : (
<p>Coming soon</p>