diff --git a/src/pages/AdminSettings.tsx b/src/pages/AdminSettings.tsx index 3fa4fc7..75e3cc8 100644 --- a/src/pages/AdminSettings.tsx +++ b/src/pages/AdminSettings.tsx @@ -57,7 +57,7 @@ interface AppRow { } export function AdminSettings({ user }: { user: User }) { - const [tab, setTab] = useState<'integrations' | 'rooms' | 'apps' | 'device' | 'mqtt-clients' | 'kiosk'>('integrations') + const [tab, setTab] = useState<'integrations' | 'rooms' | 'apps' | 'device' | 'mqtt-clients' | 'kiosk' | 'dev'>('integrations') const [integrations, setIntegrations] = useState([]) const [loading, setLoading] = useState(true) @@ -76,7 +76,7 @@ export function AdminSettings({ user }: { user: User }) {

- {(['integrations', 'rooms', 'apps', 'device', 'mqtt-clients', 'kiosk'] as const).map(t => ( + {(['integrations', 'rooms', 'apps', 'device', 'mqtt-clients', 'kiosk', 'dev'] as const).map(t => ( ))}
@@ -106,6 +106,7 @@ export function AdminSettings({ user }: { user: User }) { {tab === 'device' && } {tab === 'mqtt-clients' && } {tab === 'kiosk' && } + {tab === 'dev' && } ) } @@ -1333,6 +1334,163 @@ function KioskTab() { ) } +interface DevEntry { + at: string; user: string; endpoint: string; write: boolean + request: Record; status: number | null; ms: number | null + response: unknown; error: string | null +} + +const DEV_PRESETS: { label: string; endpoint: string; payload: Record }[] = [ + { label: 'Contact history: booking 37951', endpoint: 'contact_documents_list', payload: { recipient_type: 'bookings', recipient_id: 37951 } }, + { label: 'Booking 37951', endpoint: 'bookings_get', payload: { booking_id: 37951 } }, + { label: 'Booking custom fields', endpoint: 'custom_fields', payload: { for: 'bookings' } }, + { label: 'Contact templates', endpoint: 'contact_templates_list', payload: { return_all_types: true } }, +] + +function DevTab() { + const [endpoint, setEndpoint] = useState(DEV_PRESETS[0].endpoint) + const [payloadText, setPayloadText] = useState(JSON.stringify(DEV_PRESETS[0].payload, null, 2)) + const [allowWrite, setAllowWrite] = useState(false) + const [busy, setBusy] = useState(false) + const [result, setResult] = useState(null) + const [error, setError] = useState('') + const [history, setHistory] = useState([]) + + function loadHistory() { + fetch('/settings/api/dev/log', { credentials: 'include' }) + .then(r => r.json()).then(d => Array.isArray(d) && setHistory(d)).catch(() => {}) + } + useEffect(loadHistory, []) + + let parsed: Record | null = null + let parseError = '' + try { + const v = JSON.parse(payloadText || '{}') + if (v && typeof v === 'object' && !Array.isArray(v)) parsed = v + else parseError = 'Payload must be a JSON object' + } catch (e) { parseError = (e as Error).message } + + const isRead = /(_list|_get|_options)$|^(custom_fields|auth_test|site_list|sites_list|users_list|note_types_list)$/.test(endpoint) + const preview = parsed ? { region: '(from Newbook integration)', api_key: '••••••••', ...parsed } : null + + async function submit() { + if (!parsed) return + setBusy(true); setError(''); setResult(null) + try { + const res = await fetch('/settings/api/dev/newbook', { + method: 'POST', credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ endpoint, payload: parsed, confirm_write: !isRead && allowWrite }), + }) + const data = await res.json().catch(() => ({})) + if (!res.ok) setError(data.error || `HTTP ${res.status}`) + else setResult(data) + loadHistory() + } finally { + setBusy(false) + } + } + + const card: React.CSSProperties = { + background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: 'var(--radius)', + padding: '1.25rem', marginBottom: '1.25rem', boxShadow: 'var(--shadow-sm)', + } + const pre: React.CSSProperties = { + background: 'var(--body-bg)', border: '1px solid var(--card-border)', borderRadius: '6px', + padding: '0.75rem', fontSize: '0.78rem', overflow: 'auto', maxHeight: '420px', margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word', + } + + return ( +
+

Newbook API console

+

+ Review the request, then press Send. Region and API key are added by the server from the Newbook integration; nothing is sent until you press the button. + Read-only requests (*_list, *_get) send straight away; anything else needs the write box ticked. Responses are kept in memory only (last 100) and are not written to the server log. +

+ +
+
+ {DEV_PRESETS.map(p => ( + + ))} +
+ + +