feat: agent version tracking, update button, device test, COM port list
- Migration 043: agent_version in workstations, netup device type - agent-ws: sendCommandAndWait for async commands, save version on hello - workstations route: list-ports, test-device, update, update-all endpoints - agent-release route: GET /api/agents/latest-release reads latest.yml - app.ts: serve /agent-updates/ static files - api.ts: fix camelCase→snake_case for outgoing requests (fixes null:null) - api.ts: new methods testDevice, sendUpdate, updateAll, listPorts - EquipmentPage: version badge, update button, test (zap) button per device - EquipmentPage: COM-port dropdown loaded from agent when online Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
23
backend/src/routes/agent-release.ts
Normal file
23
backend/src/routes/agent-release.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
const agentRelease: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.get('/api/agents/latest-release', { onRequest: [fastify.authenticate] }, async (_req, reply) => {
|
||||
const updatesDir = process.env.AGENT_UPDATES_DIR ?? join(process.cwd(), '..', 'agent-updates')
|
||||
try {
|
||||
const yaml = await readFile(join(updatesDir, 'latest.yml'), 'utf8')
|
||||
// Parse version: line like "version: 1.0.6"
|
||||
const versionMatch = yaml.match(/^version:\s*(.+)$/m)
|
||||
const pathMatch = yaml.match(/^path:\s*(.+)$/m)
|
||||
if (!versionMatch) return reply.code(404).send({ error: 'latest.yml not found or invalid' })
|
||||
const version = versionMatch[1].trim()
|
||||
const fileName = pathMatch?.[1].trim() ?? `HotelSync Agent Setup ${version}.exe`
|
||||
return { version, fileName, downloadUrl: `/agent-updates/${encodeURIComponent(fileName)}` }
|
||||
} catch {
|
||||
return reply.code(404).send({ error: 'No release available' })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default agentRelease
|
||||
@@ -16,6 +16,7 @@
|
||||
import { FastifyPluginAsync } from 'fastify'
|
||||
import { db } from '../db'
|
||||
import crypto from 'crypto'
|
||||
import { sendCommand, sendCommandAndWait } from '../agent-ws'
|
||||
|
||||
type SlugParam = { Params: { slug: string } }
|
||||
type WsParam = { Params: { slug: string; id: string } }
|
||||
@@ -258,6 +259,80 @@ const workstationRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
return reply.code(204).send()
|
||||
},
|
||||
)
|
||||
|
||||
// ── GET /api/hotels/:slug/workstations/:id/ports ────────────────────────────
|
||||
fastify.get<WsParam>(
|
||||
'/api/hotels/:slug/workstations/:id/ports',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (req, reply) => {
|
||||
const { id } = req.params
|
||||
try {
|
||||
const result = await sendCommandAndWait(id, { type: 'list_ports' }, 8000) as { ok: boolean; ports?: { port: string; description?: string }[]; error?: string }
|
||||
if (!result.ok) return reply.code(502).send({ error: result.error ?? 'Агент недоступен' })
|
||||
return { ports: result.ports ?? [] }
|
||||
} catch (err) {
|
||||
return reply.code(502).send({ error: err instanceof Error ? err.message : 'Ошибка' })
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// ── POST /api/hotels/:slug/workstations/:id/test-device/:deviceId ───────────
|
||||
fastify.post<DevParam>(
|
||||
'/api/hotels/:slug/workstations/:id/test-device/:deviceId',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (req, reply) => {
|
||||
const { id, deviceId } = req.params
|
||||
const { rows: [device] } = await db.query('SELECT * FROM workstation_devices WHERE id = $1', [deviceId])
|
||||
if (!device) return reply.code(404).send({ error: 'Device not found' })
|
||||
|
||||
try {
|
||||
const result = await sendCommandAndWait(id, {
|
||||
type: 'test_device',
|
||||
device: {
|
||||
id: device.id,
|
||||
type: device.type,
|
||||
connection: device.connection,
|
||||
network_host: device.network_host,
|
||||
network_port: device.network_port,
|
||||
com_port: device.com_port,
|
||||
},
|
||||
}) as { ok: boolean; error?: string; message?: string }
|
||||
return result
|
||||
} catch (err) {
|
||||
return reply.code(502).send({ ok: false, error: err instanceof Error ? err.message : 'Ошибка' })
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// ── POST /api/hotels/:slug/workstations/:id/update ──────────────────────────
|
||||
fastify.post<WsParam>(
|
||||
'/api/hotels/:slug/workstations/:id/update',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (req, reply) => {
|
||||
const { id } = req.params
|
||||
const r = await sendCommand(id, { type: 'update_now' })
|
||||
if (!r.ok) return reply.code(502).send({ error: r.error ?? 'Агент недоступен' })
|
||||
return { ok: true }
|
||||
},
|
||||
)
|
||||
|
||||
// ── POST /api/hotels/:slug/workstations/update-all ──────────────────────────
|
||||
fastify.post<SlugParam>(
|
||||
'/api/hotels/:slug/workstations/update-all',
|
||||
{ onRequest: [fastify.authenticate] },
|
||||
async (req, reply) => {
|
||||
const { slug } = req.params
|
||||
const hotelId = await getHotelId(slug)
|
||||
if (!hotelId) return reply.code(404).send({ error: 'Not found' })
|
||||
const { rows } = await db.query('SELECT id FROM workstations WHERE hotel_id = $1 AND is_online = true', [hotelId])
|
||||
let sent = 0
|
||||
for (const ws of rows) {
|
||||
const r = await sendCommand(ws.id, { type: 'update_now' })
|
||||
if (r.ok) sent++
|
||||
}
|
||||
return { ok: true, sent }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export default workstationRoutes
|
||||
|
||||
Reference in New Issue
Block a user