Add TravelLine pull integration for NetUP: capture endpoint + tl_token

- New /travelline route: accepts all NetUP polls, logs them to in-memory
  ring buffer (GET /travelline/_log to inspect), responds with active
  reservations in a generic PMS format
- Migration 007: adds tl_token column to netup_settings
- PATCH netup/settings now saves tl_token; GET returns it
- TvWelcomePage: new TravelLine section with read-only API URL + copy
  button and token input field

Setup in NetUP: Integration type = TravelLine,
API URL = https://api.hotelsync.ru/travelline, Token = <tl_token>

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 18:57:10 +03:00
parent 05d0c94a9a
commit 8350309645
6 changed files with 259 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { Tv2, Plug, Map, CheckCircle2, XCircle, Loader2, Save, Eye, EyeOff } from 'lucide-react'
import { Tv2, Plug, Map, CheckCircle2, XCircle, Loader2, Save, Eye, EyeOff, Copy, Link2 } from 'lucide-react'
import { cn } from '../lib/utils'
import { api } from '../lib/api'
import { useAuth } from '../contexts/AuthContext'
@@ -30,7 +30,9 @@ export function TvWelcomePage() {
const [password, setPassword] = useState('')
const [language, setLanguage] = useState('ru_RU')
const [enabled, setEnabled] = useState(true)
const [tlToken, setTlToken] = useState('')
const [settingsLoaded, setSettingsLoaded] = useState(false)
const [copied, setCopied] = useState(false)
// Room mappings
const [rooms, setRooms] = useState<{ id: string; number: string; type: string; netupRoomNumber: string }[]>([])
@@ -46,6 +48,7 @@ export function TvWelcomePage() {
setUsername(s.username)
setLanguage(s.defaultLanguage)
setEnabled(s.enabled)
setTlToken(s.tlToken ?? '')
setSettingsLoaded(true)
})
.catch(() => setSettingsLoaded(true))
@@ -69,6 +72,7 @@ export function TvWelcomePage() {
password: password || undefined,
default_language: language,
enabled,
tl_token: tlToken,
})
setPassword('')
} finally {
@@ -86,6 +90,7 @@ export function TvWelcomePage() {
password: password || undefined,
default_language: language,
enabled,
tl_token: tlToken,
})
const res = await api.netup.testConnection(slug)
setTestResult({ ok: true, msg: res.message })
@@ -244,6 +249,65 @@ export function TvWelcomePage() {
</p>
</div>
<hr className="border-slate-100 dark:border-slate-700" />
{/* TravelLine / Pull integration */}
<div className="space-y-3">
<div>
<p className="font-medium text-slate-900 dark:text-slate-100 text-sm">
Интеграция через TravelLine (pull-режим)
</p>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Альтернативный метод: NetUP сам опрашивает наш сервер. Настройте в NetUP тип интеграции «TravelLine» и укажите адрес ниже.
</p>
</div>
{/* API URL (read-only) */}
<div className="space-y-1.5">
<label className="text-sm font-medium text-slate-700 dark:text-slate-300 flex items-center gap-1.5">
<Link2 size={13} />
API URL для NetUP
</label>
<div className="flex gap-2">
<input
type="text"
readOnly
value="https://api.hotelsync.ru/travelline"
className="input w-full font-mono text-xs bg-slate-50 dark:bg-slate-700/50 text-slate-500 dark:text-slate-400"
/>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText('https://api.hotelsync.ru/travelline')
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}}
className="btn-secondary px-3 shrink-0 flex items-center gap-1.5"
>
{copied ? <CheckCircle2 size={13} className="text-emerald-500" /> : <Copy size={13} />}
{copied ? 'Скопировано' : 'Копировать'}
</button>
</div>
</div>
{/* Token */}
<div className="space-y-1.5">
<label className="text-sm font-medium text-slate-700 dark:text-slate-300">
Токен авторизации
</label>
<input
type="text"
value={tlToken}
onChange={e => setTlToken(e.target.value)}
placeholder="Придумайте любой секретный токен"
className="input w-full font-mono text-sm"
/>
<p className="text-xs text-slate-400 dark:text-slate-500">
Введите этот же токен в поле «Токен» настроек TravelLine в NetUP
</p>
</div>
</div>
{/* Test result */}
{testResult && (
<div className={cn(