Add push monitor script deployment via SSH
All checks were successful
Build and Push Container / build (push) Successful in 33s
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:
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user