feat: ping through agent — send ICMP ping from workstation via agent
Adds a ping tool in the Equipment page workstation card (visible when agent is online). User enters an IP/hostname, the request goes through the PMS backend → WebSocket → agent → executes ping -n 4, returns raw output displayed in a monospace block. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -343,6 +343,23 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── POST /api/hotels/:slug/workstations/:id/ping ────────────────────────────
|
||||||
|
fastify.post<WsParam & { Body: { host: string } }>(
|
||||||
|
'/api/hotels/:slug/workstations/:id/ping',
|
||||||
|
{ onRequest: [fastify.authenticate] },
|
||||||
|
async (req, reply) => {
|
||||||
|
const { id } = req.params
|
||||||
|
const { host } = req.body
|
||||||
|
if (!host?.trim()) return reply.code(400).send({ error: 'host required' })
|
||||||
|
try {
|
||||||
|
const result = await sendCommandAndWait(id, { type: 'ping_host', host: host.trim() }, 20000) as { ok: boolean; output?: string; error?: string }
|
||||||
|
return result
|
||||||
|
} catch (err) {
|
||||||
|
return reply.code(502).send({ ok: false, error: err instanceof Error ? err.message : 'Ошибка' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// ── POST /api/hotels/:slug/workstations/update-all ──────────────────────────
|
// ── POST /api/hotels/:slug/workstations/update-all ──────────────────────────
|
||||||
fastify.post<SlugParam>(
|
fastify.post<SlugParam>(
|
||||||
'/api/hotels/:slug/workstations/update-all',
|
'/api/hotels/:slug/workstations/update-all',
|
||||||
|
|||||||
@@ -361,6 +361,8 @@ export const api = {
|
|||||||
req<{ ports: { port: string; description?: string }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/ports`),
|
req<{ ports: { port: string; description?: string }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/ports`),
|
||||||
listPrinters: (slug: string, wsId: string) =>
|
listPrinters: (slug: string, wsId: string) =>
|
||||||
req<{ printers: { name: string; isDefault: boolean }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/printers`),
|
req<{ printers: { name: string; isDefault: boolean }[] }>('GET', `/api/hotels/${slug}/workstations/${wsId}/printers`),
|
||||||
|
ping: (slug: string, wsId: string, host: string) =>
|
||||||
|
req<{ ok: boolean; output?: string; error?: string }>('POST', `/api/hotels/${slug}/workstations/${wsId}/ping`, { host }),
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Agents ────────────────────────────────────────────────────────────────
|
// ── Agents ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -599,6 +599,9 @@ function WorkstationCard({
|
|||||||
const [updateMsg, setUpdateMsg] = useState<string | null>(null)
|
const [updateMsg, setUpdateMsg] = useState<string | null>(null)
|
||||||
const [testResults, setTestResults] = useState<Record<string, { ok: boolean; msg: string } | null>>({})
|
const [testResults, setTestResults] = useState<Record<string, { ok: boolean; msg: string } | null>>({})
|
||||||
const [testingDeviceId, setTestingDeviceId] = useState<string | null>(null)
|
const [testingDeviceId, setTestingDeviceId] = useState<string | null>(null)
|
||||||
|
const [pingHost, setPingHost] = useState('')
|
||||||
|
const [pinging, setPinging] = useState(false)
|
||||||
|
const [pingResult, setPingResult] = useState<{ ok: boolean; output?: string; error?: string } | null>(null)
|
||||||
|
|
||||||
const hasUpdate = !!(
|
const hasUpdate = !!(
|
||||||
ws.isOnline &&
|
ws.isOnline &&
|
||||||
@@ -676,6 +679,20 @@ function WorkstationCard({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pingNow = async () => {
|
||||||
|
if (!pingHost.trim() || pinging) return
|
||||||
|
setPinging(true)
|
||||||
|
setPingResult(null)
|
||||||
|
try {
|
||||||
|
const r = await api.workstations.ping(slug, ws.id, pingHost.trim())
|
||||||
|
setPingResult(r)
|
||||||
|
} catch (err) {
|
||||||
|
setPingResult({ ok: false, error: err instanceof Error ? err.message : 'Ошибка' })
|
||||||
|
} finally {
|
||||||
|
setPinging(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const devices = ws.devices ?? []
|
const devices = ws.devices ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -905,6 +922,44 @@ function WorkstationCard({
|
|||||||
Добавить устройство
|
Добавить устройство
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Ping tool — only when agent is online */}
|
||||||
|
{ws.isOnline && (
|
||||||
|
<div className="pt-2 border-t border-slate-100 dark:border-slate-700">
|
||||||
|
<p className="text-xs text-slate-400 dark:text-slate-500 mb-2">Пинг через агента</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
value={pingHost}
|
||||||
|
onChange={e => setPingHost(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === 'Enter' && pingNow()}
|
||||||
|
placeholder="192.168.1.1 или hostname"
|
||||||
|
className="input text-sm font-mono flex-1 py-1.5"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={pingNow}
|
||||||
|
disabled={pinging || !pingHost.trim()}
|
||||||
|
className="btn-secondary py-1.5 px-3 text-sm flex items-center gap-1.5 shrink-0 h-9 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{pinging ? <RefreshCw size={13} className="animate-spin" /> : <Network size={13} />}
|
||||||
|
{pinging ? 'Пинг...' : 'Пинг'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{pingResult && (
|
||||||
|
<div className={cn(
|
||||||
|
'mt-2 rounded-lg text-xs',
|
||||||
|
pingResult.ok
|
||||||
|
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400'
|
||||||
|
: 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400',
|
||||||
|
)}>
|
||||||
|
{pingResult.ok && pingResult.output ? (
|
||||||
|
<pre className="p-3 whitespace-pre-wrap font-mono text-xs overflow-x-auto">{pingResult.output.trim()}</pre>
|
||||||
|
) : (
|
||||||
|
<p className="p-3 flex items-center gap-1.5"><X size={11} />{pingResult.error ?? 'Ошибка'}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user