feat: TTLock diagnostics — test cloud API button + fix api_server SELECT bug
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,45 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── POST /api/hotels/:slug/ttlock/test-api ──────────────────────────────────
|
||||||
|
// Проверяет подключение к TTLock Cloud API (OAuth + список замков)
|
||||||
|
fastify.post<SlugParam & { Body: { workstation_id: string } }>(
|
||||||
|
'/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 ──────────────────────────────────────
|
// ── POST /api/hotels/:slug/ttlock/test ──────────────────────────────────────
|
||||||
// Проверяем: агент онлайн, энкодер отвечает на COM-порту
|
// Проверяем: агент онлайн, энкодер отвечает на COM-порту
|
||||||
fastify.post<SlugParam & { Body: { workstation_id: string } }>(
|
fastify.post<SlugParam & { Body: { workstation_id: string } }>(
|
||||||
@@ -272,7 +311,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => {
|
|||||||
|
|
||||||
// Конфигурация TTHotel (client_id, card_sectors)
|
// Конфигурация TTHotel (client_id, card_sectors)
|
||||||
const { rows: cfgRows } = await db.query(
|
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],
|
[hotelId],
|
||||||
)
|
)
|
||||||
const cfg = cfgRows[0]
|
const cfg = cfgRows[0]
|
||||||
|
|||||||
@@ -616,6 +616,9 @@ export const api = {
|
|||||||
testEncoder: (slug: string, workstationId: string) =>
|
testEncoder: (slug: string, workstationId: string) =>
|
||||||
req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test`, { workstationId }),
|
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) =>
|
dllInfo: (slug: string, workstationId: string) =>
|
||||||
req<{ ok: boolean; content: string; path: string }>('GET', `/api/hotels/${slug}/ttlock/dll-info?workstation_id=${workstationId}`),
|
req<{ ok: boolean; content: string; path: string }>('GET', `/api/hotels/${slug}/ttlock/dll-info?workstation_id=${workstationId}`),
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as XLSX from 'xlsx'
|
|||||||
import {
|
import {
|
||||||
KeyRound, Save, RefreshCw, Check, X, AlertCircle,
|
KeyRound, Save, RefreshCw, Check, X, AlertCircle,
|
||||||
ChevronDown, ChevronRight, Trash2, Plus, Eye, EyeOff,
|
ChevronDown, ChevronRight, Trash2, Plus, Eye, EyeOff,
|
||||||
Lock, Unlock, Info, HardDriveDownload, Zap, Terminal, Upload,
|
Lock, Unlock, Info, HardDriveDownload, Zap, Terminal, Upload, Wifi,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { api, type TTLockConfig, type RoomLockMapping, type Workstation } from '../lib/api'
|
import { api, type TTLockConfig, type RoomLockMapping, type Workstation } from '../lib/api'
|
||||||
import { useAuth } from '../contexts/AuthContext'
|
import { useAuth } from '../contexts/AuthContext'
|
||||||
@@ -134,6 +134,7 @@ export function TTLockPage() {
|
|||||||
const [workstations, setWorkstations] = useState<Workstation[]>([])
|
const [workstations, setWorkstations] = useState<Workstation[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const [testingApi, setTestingApi] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [success, setSuccess] = useState<string | null>(null)
|
const [success, setSuccess] = useState<string | null>(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 () => {
|
const handleAddMapping = async () => {
|
||||||
if (!addRoomId || !addLockMac.trim()) return
|
if (!addRoomId || !addLockMac.trim()) return
|
||||||
setAddLoading(true)
|
setAddLoading(true)
|
||||||
@@ -435,10 +451,16 @@ export function TTLockPage() {
|
|||||||
<SectorPicker value={cardSectors} onChange={setCardSectors} />
|
<SectorPicker value={cardSectors} onChange={setCardSectors} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button onClick={handleSave} disabled={saving} className="btn-primary flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{saving ? <RefreshCw size={15} className="animate-spin" /> : <Save size={15} />}
|
<button onClick={handleTestApi} disabled={testingApi || saving} className="btn-secondary flex items-center gap-2">
|
||||||
Сохранить
|
{testingApi ? <RefreshCw size={15} className="animate-spin" /> : <Wifi size={15} />}
|
||||||
</button>
|
Проверить API
|
||||||
|
</button>
|
||||||
|
<button onClick={handleSave} disabled={saving} className="btn-primary flex items-center gap-2">
|
||||||
|
{saving ? <RefreshCw size={15} className="animate-spin" /> : <Save size={15} />}
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user