NewBook credentials from central Settings service

Stack-wide NewBook config lives in the Settings app (LXC 116) and is
fetched live via SETTINGS_URL/SETTINGS_SECRET — same pattern as cashup,
room-planner and maintenance. App-local system_config credentials remain
as a fallback for standalone/dev use. The app's Settings → Newbook page
no longer edits credentials; it points to the central app and keeps
Test Connection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-04 19:36:41 +00:00
parent 851747b561
commit aeb99650bd
10 changed files with 226 additions and 230 deletions

View file

@ -2024,61 +2024,9 @@ const CurrentRatesDataSyncSection: React.FC = () => {
}
const NewbookPage: React.FC = () => {
const [apiKey, setApiKey] = useState('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [region, setRegion] = useState('')
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle')
const [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle')
const [testMessage, setTestMessage] = useState('')
// Fetch current settings
const { data: settings, isLoading } = useQuery({
queryKey: ['newbook-settings'],
queryFn: async () => {
const response = await fetch('/forecasting/api/config/settings/newbook')
if (!response.ok) throw new Error('Failed to fetch settings')
return response.json() as Promise<NewbookSettings>
},
staleTime: 30000,
})
// Populate form when settings load
React.useEffect(() => {
if (settings) {
setUsername(settings.newbook_username || '')
setRegion(settings.newbook_region || '')
// Don't populate password/api_key - they're masked
}
}, [settings])
const handleSave = async () => {
setSaveStatus('saving')
try {
const response = await fetch('/forecasting/api/config/settings/newbook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
newbook_api_key: apiKey || undefined,
newbook_username: username || undefined,
newbook_password: password || undefined,
newbook_region: region || undefined,
})
})
if (!response.ok) throw new Error('Failed to save')
setSaveStatus('success')
// Clear password fields after save
setApiKey('')
setPassword('')
setTimeout(() => setSaveStatus('idle'), 3000)
} catch {
setSaveStatus('error')
setTimeout(() => setSaveStatus('idle'), 3000)
}
}
const handleTestConnection = async () => {
setTestStatus('testing')
setTestMessage('')
@ -2104,146 +2052,38 @@ const NewbookPage: React.FC = () => {
}, 5000)
}
if (isLoading) {
return (
<div style={styles.section}>
<div style={styles.loading}>Loading settings...</div>
</div>
)
}
return (
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Newbook Settings</h2>
<p style={styles.hint}>Configure your Newbook API connection for hotel data synchronization.</p>
<p style={styles.hint}>Newbook data synchronization for this app.</p>
<div style={styles.apiConfigRow}>
{/* Left side - API Configuration */}
<div style={styles.apiConfigLeft}>
<h3 style={styles.subsectionTitle}>API Configuration</h3>
<div style={styles.form}>
<label style={styles.label}>
<span>API Key</span>
<div style={styles.inputWithStatus}>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder={settings?.newbook_api_key_set ? '••••••••' : 'Enter API key'}
style={styles.input}
/>
{settings?.newbook_api_key_set && (
<span style={styles.keyStatus}>Key configured</span>
)}
</div>
</label>
<label style={styles.label}>
<span>Username</span>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Newbook account username"
style={styles.input}
/>
</label>
<label style={styles.label}>
<span>Password</span>
<div style={styles.inputWithStatus}>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={settings?.newbook_password_set ? '••••••••' : 'Enter password'}
style={styles.input}
/>
{settings?.newbook_password_set && (
<span style={styles.keyStatus}>Password configured</span>
)}
</div>
</label>
<label style={styles.label}>
<span>Region</span>
<input
type="text"
value={region}
onChange={(e) => setRegion(e.target.value)}
placeholder="e.g., uk, au"
style={styles.input}
/>
</label>
<div style={styles.buttonRow}>
<button
onClick={handleSave}
disabled={saveStatus === 'saving'}
style={mergeStyles(
buttonStyle('primary'),
saveStatus === 'success' ? { background: colors.success } : {},
saveStatus === 'error' ? { background: colors.error } : {}
)}
>
{saveStatus === 'saving' ? 'Saving...' :
saveStatus === 'success' ? 'Saved!' :
saveStatus === 'error' ? 'Error' : 'Save Settings'}
</button>
<button
onClick={handleTestConnection}
disabled={testStatus === 'testing'}
style={buttonStyle('outline')}
>
{testStatus === 'testing' ? 'Testing...' : 'Test Connection'}
</button>
</div>
{testMessage && (
<div style={{
...styles.statusMessage,
background: testStatus === 'success' ? colors.successBg : colors.errorBg,
color: testStatus === 'success' ? colors.success : colors.error,
}}>
{testMessage}
</div>
)}
</div>
</div>
{/* Right side - Connection Status */}
<div style={styles.apiConfigRight}>
<h3 style={styles.subsectionTitle}>Connection Status</h3>
<div style={styles.statusGridVertical}>
<div style={styles.statusItem}>
<span style={styles.statusLabel}>API Key</span>
<span style={settings?.newbook_api_key_set ? styles.statusOk : styles.statusPending}>
{settings?.newbook_api_key_set ? 'Configured' : 'Not set'}
</span>
</div>
<div style={styles.statusItem}>
<span style={styles.statusLabel}>Username</span>
<span style={settings?.newbook_username ? styles.statusOk : styles.statusPending}>
{settings?.newbook_username || 'Not set'}
</span>
</div>
<div style={styles.statusItem}>
<span style={styles.statusLabel}>Password</span>
<span style={settings?.newbook_password_set ? styles.statusOk : styles.statusPending}>
{settings?.newbook_password_set ? 'Configured' : 'Not set'}
</span>
</div>
<div style={styles.statusItem}>
<span style={styles.statusLabel}>Region</span>
<span style={settings?.newbook_region ? styles.statusOk : styles.statusPending}>
{settings?.newbook_region || 'Not set'}
</span>
</div>
</div>
</div>
<div style={styles.infoBox}>
Newbook API credentials are managed centrally in the stack <strong>Settings</strong> app
(Integrations NewBook) and shared by all apps. Use the button below to verify this
app can reach Newbook with those credentials.
</div>
<div style={styles.buttonRow}>
<button
onClick={handleTestConnection}
disabled={testStatus === 'testing'}
style={buttonStyle('outline')}
>
{testStatus === 'testing' ? 'Testing...' : 'Test Connection'}
</button>
</div>
{testMessage && (
<div style={{
...styles.statusMessage,
background: testStatus === 'success' ? colors.successBg : colors.errorBg,
color: testStatus === 'success' ? colors.success : colors.error,
marginTop: spacing.md,
}}>
{testMessage}
</div>
)}
<div style={styles.divider} />
<RoomCategoriesSection />