From 54469e7ad12590964dbff206c1c93f95451e128c Mon Sep 17 00:00:00 2001 From: HotelSync Date: Thu, 2 Apr 2026 00:45:32 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20TTLock=20diagnostics=20=E2=80=94=20test?= =?UTF-8?q?=20cloud=20API=20button=20+=20fix=20api=5Fserver=20SELECT=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix: api_server was missing from SELECT in issue-card route (always fell back to EU default) - Add "Проверить API" button in Step 1 that calls ttlock:test_api via agent - Agent: testCloudApi() — gets OAuth token + lists locks, returns count - Agent: apiPost() now includes response body in error message (was just HTTP status) Co-Authored-By: Claude Sonnet 4.6 --- backend/src/routes/ttlock.ts | 41 +++++++++++++++++++++++++++++++++++- src/lib/api.ts | 3 +++ src/pages/TTLockPage.tsx | 32 +++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/backend/src/routes/ttlock.ts b/backend/src/routes/ttlock.ts index 3f3d192..fdcf2cc 100644 --- a/backend/src/routes/ttlock.ts +++ b/backend/src/routes/ttlock.ts @@ -131,6 +131,45 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { }, ) + // ── POST /api/hotels/:slug/ttlock/test-api ────────────────────────────────── + // Проверяет подключение к TTLock Cloud API (OAuth + список замков) + fastify.post( + '/api/hotels/:slug/ttlock/test-api', + { onRequest: [fastify.authenticate] }, + async (req, reply) => { + const { slug } = req.params + if (!canManage(req.user.hotelSlug, req.user.role, slug)) + return reply.code(403).send({ error: 'Forbidden' }) + const hotelId = await getHotelId(slug) + if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) + + const { rows: cfgRows } = await db.query( + 'SELECT client_id, client_secret, api_server FROM hotel_ttlock_config WHERE hotel_id = $1', + [hotelId], + ) + const cfg = cfgRows[0] + if (!cfg?.client_id) return reply.code(400).send({ error: 'client_id не заполнен' }) + + const workstationId = req.body.workstation_id + if (!workstationId) return reply.code(400).send({ error: 'workstation_id required' }) + + try { + const result = await sendCommandAndWait(workstationId, { + type: 'ttlock:test_api', + clientId: cfg.client_id, + clientSecret: cfg.client_secret, + apiServer: cfg.api_server ?? 'https://euopen.ttlock.com', + }, 15_000) as { ok?: boolean; lockCount?: number; error?: string } + + if (!result.ok) return reply.code(502).send({ error: result.error ?? 'Ошибка API' }) + return { ok: true, lockCount: result.lockCount } + } catch (err) { + const msg = err instanceof Error ? err.message : 'Агент не ответил' + return reply.code(502).send({ error: msg }) + } + }, + ) + // ── POST /api/hotels/:slug/ttlock/test ────────────────────────────────────── // Проверяем: агент онлайн, энкодер отвечает на COM-порту fastify.post( @@ -272,7 +311,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { // Конфигурация TTHotel (client_id, card_sectors) const { rows: cfgRows } = await db.query( - 'SELECT client_id, client_secret, card_sectors, is_enabled FROM hotel_ttlock_config WHERE hotel_id = $1', + 'SELECT client_id, client_secret, card_sectors, is_enabled, api_server FROM hotel_ttlock_config WHERE hotel_id = $1', [hotelId], ) const cfg = cfgRows[0] diff --git a/src/lib/api.ts b/src/lib/api.ts index 8b04f03..310b620 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -616,6 +616,9 @@ export const api = { testEncoder: (slug: string, workstationId: string) => req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test`, { workstationId }), + testApi: (slug: string, workstationId: string) => + req<{ ok: boolean; lockCount?: number }>('POST', `/api/hotels/${slug}/ttlock/test-api`, { workstation_id: workstationId }), + dllInfo: (slug: string, workstationId: string) => req<{ ok: boolean; content: string; path: string }>('GET', `/api/hotels/${slug}/ttlock/dll-info?workstation_id=${workstationId}`), diff --git a/src/pages/TTLockPage.tsx b/src/pages/TTLockPage.tsx index 88b7551..80b8f30 100644 --- a/src/pages/TTLockPage.tsx +++ b/src/pages/TTLockPage.tsx @@ -3,7 +3,7 @@ import * as XLSX from 'xlsx' import { KeyRound, Save, RefreshCw, Check, X, AlertCircle, ChevronDown, ChevronRight, Trash2, Plus, Eye, EyeOff, - Lock, Unlock, Info, HardDriveDownload, Zap, Terminal, Upload, + Lock, Unlock, Info, HardDriveDownload, Zap, Terminal, Upload, Wifi, } from 'lucide-react' import { api, type TTLockConfig, type RoomLockMapping, type Workstation } from '../lib/api' import { useAuth } from '../contexts/AuthContext' @@ -134,6 +134,7 @@ export function TTLockPage() { const [workstations, setWorkstations] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) + const [testingApi, setTestingApi] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) @@ -277,6 +278,21 @@ export function TTLockPage() { } } + const handleTestApi = async () => { + const ws = workstations[0] + if (!ws) { setError('Нет рабочих мест — добавьте на странице «Оборудование»'); return } + setTestingApi(true) + setError(null) + try { + const result = await api.ttlock.testApi(slug, ws.id) + showSuccess(`TTLock Cloud API работает — найдено замков: ${result.lockCount ?? 0}`) + } catch (e) { + setError(e instanceof Error ? e.message : 'Ошибка проверки API') + } finally { + setTestingApi(false) + } + } + const handleAddMapping = async () => { if (!addRoomId || !addLockMac.trim()) return setAddLoading(true) @@ -435,10 +451,16 @@ export function TTLockPage() { - +
+ + +