Add push monitor script deployment via SSH
All checks were successful
Build and Push Container / build (push) Successful in 33s

- Add push_scripts.py with bash templates for heartbeat, disk, memory, cpu, and updates monitoring
- Modify kuma_client.py to return push token from created monitors
- Add deploy_push_script() to monitors.py for SSH-based script deployment
- Add heartbeat push_metric type to Claude agent suggestions
- Add /api/monitors/<id>/deploy-script and /api/monitors/deploy-all-scripts endpoints
- Update frontend to show push monitors with deployment status and retry buttons

Scripts are deployed to /usr/local/bin/kuma-push-{metric}-{id}.sh with cron entries.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-01-05 08:36:48 +00:00
parent 908d147235
commit ae814c1aea
7 changed files with 577 additions and 21 deletions

View File

@@ -58,6 +58,20 @@ export const api = {
method: 'POST',
body: JSON.stringify({ scan_id: scanId, monitors: monitorIndices }),
}),
deployPushScript: (monitorId, hostname, pushMetric, options = {}) => fetchApi(`/monitors/${monitorId}/deploy-script`, {
method: 'POST',
body: JSON.stringify({
hostname,
push_metric: pushMetric,
username: options.username || 'root',
port: options.port || 22,
interval_minutes: options.intervalMinutes || 5,
}),
}),
deployAllPushScripts: (monitors) => fetchApi('/monitors/deploy-all-scripts', {
method: 'POST',
body: JSON.stringify({ monitors }),
}),
// Uptime Kuma
testKumaConnection: () => fetchApi('/kuma/test'),

View File

@@ -8,6 +8,7 @@ export default function DiscoveryResults({ scanId, scan, analysis, devMode, onCo
const [createResults, setCreateResults] = useState(null);
const [runningCommands, setRunningCommands] = useState({});
const [questionAnswers, setQuestionAnswers] = useState({});
const [deployingScripts, setDeployingScripts] = useState({});
const handleRunCommand = async (command, index) => {
setRunningCommands(prev => ({ ...prev, [index]: true }));
@@ -65,6 +66,38 @@ export default function DiscoveryResults({ scanId, scan, analysis, devMode, onCo
);
};
const handleRetryDeploy = async (resultIndex, result) => {
const monitorId = result.result?.monitorID;
const pushMetric = result.push_metric;
if (!monitorId || !pushMetric) {
console.error('Missing monitor ID or push metric for deployment');
return;
}
setDeployingScripts(prev => ({ ...prev, [resultIndex]: true }));
try {
const deployResult = await api.deployPushScript(
monitorId,
scan.hostname,
pushMetric,
{ port: scan.port || 22 }
);
// Update the result with new deployment status
setCreateResults(prev => prev.map((r, i) =>
i === resultIndex ? { ...r, deployment: deployResult } : r
));
} catch (err) {
console.error('Failed to deploy script:', err);
setCreateResults(prev => prev.map((r, i) =>
i === resultIndex ? { ...r, deployment: { status: 'failed', error: err.message } } : r
));
} finally {
setDeployingScripts(prev => ({ ...prev, [resultIndex]: false }));
}
};
if (!scan.connected) {
return (
<div className="bg-red-900/30 border border-red-600 rounded-lg p-6">
@@ -190,9 +223,14 @@ export default function DiscoveryResults({ scanId, scan, analysis, devMode, onCo
monitor.type === 'http' ? 'bg-blue-900 text-blue-200' :
monitor.type === 'tcp' ? 'bg-green-900 text-green-200' :
monitor.type === 'docker' ? 'bg-cyan-900 text-cyan-200' :
monitor.type === 'push' ? 'bg-purple-900 text-purple-200' :
monitor.type === 'ping' ? 'bg-emerald-900 text-emerald-200' :
'bg-slate-600 text-slate-200'
}`}>
{monitor.type.toUpperCase()}
{monitor.type === 'push' && monitor.push_metric && (
<span className="ml-1 opacity-75">({monitor.push_metric})</span>
)}
</span>
<span className="font-medium">{monitor.name}</span>
</div>
@@ -300,25 +338,82 @@ export default function DiscoveryResults({ scanId, scan, analysis, devMode, onCo
{createResults.map((result, index) => (
<div
key={index}
className={`p-3 rounded flex items-center gap-2 ${
className={`p-3 rounded ${
result.status === 'created'
? 'bg-green-900/30 text-green-300'
: 'bg-red-900/30 text-red-300'
}`}
>
{result.status === 'created' ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
<div className="flex items-center gap-2">
{result.status === 'created' ? (
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
<span className="font-medium">{result.monitor}</span>
<span className="text-sm opacity-75">({result.type})</span>
{result.error && (
<span className="text-sm ml-auto">{result.error}</span>
)}
</div>
{/* Push monitor deployment status */}
{result.type === 'push' && result.deployment && (
<div className={`mt-2 ml-7 text-sm ${
result.deployment.status === 'deployed' ? 'text-green-400' :
result.deployment.status === 'failed' ? 'text-red-400' : 'text-slate-400'
}`}>
<div className="flex items-center gap-2">
{result.deployment.status === 'deployed' ? (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Script deployed to {result.deployment.script_path}</span>
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>Deployment failed: {result.deployment.error}</span>
<button
onClick={() => handleRetryDeploy(index, result)}
disabled={deployingScripts[index]}
className="ml-2 px-2 py-0.5 text-xs bg-purple-600 hover:bg-purple-500 disabled:bg-slate-600 text-white rounded transition-colors"
>
{deployingScripts[index] ? 'Deploying...' : 'Retry Deploy'}
</button>
</>
)}
</div>
{result.deployment.cronjob && (
<div className="mt-1 text-xs text-slate-500 font-mono">
Cronjob: {result.deployment.cronjob}
</div>
)}
</div>
)}
<span className="font-medium">{result.monitor}</span>
<span className="text-sm opacity-75">({result.type})</span>
{result.error && (
<span className="text-sm ml-auto">{result.error}</span>
{/* Push monitor without deployment info (needs manual deploy) */}
{result.type === 'push' && result.status === 'created' && !result.deployment && (
<div className="mt-2 ml-7 text-sm text-amber-400 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Script not deployed</span>
<button
onClick={() => handleRetryDeploy(index, result)}
disabled={deployingScripts[index]}
className="ml-2 px-2 py-0.5 text-xs bg-purple-600 hover:bg-purple-500 disabled:bg-slate-600 text-white rounded transition-colors"
>
{deployingScripts[index] ? 'Deploying...' : 'Deploy Script'}
</button>
</div>
)}
</div>
))}