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:
parent
851747b561
commit
aeb99650bd
10 changed files with 226 additions and 230 deletions
|
|
@ -496,9 +496,19 @@ def _refresh_date_sync(rate_date: date):
|
||||||
)
|
)
|
||||||
config = {row.config_key: row.config_value for row in config_result.fetchall()}
|
config = {row.config_key: row.config_value for row in config_result.fetchall()}
|
||||||
|
|
||||||
|
# Central Settings service first, app-local config fallback
|
||||||
|
from services.central_settings import get_newbook_credentials_sync
|
||||||
|
creds = get_newbook_credentials_sync()
|
||||||
|
if not creds:
|
||||||
if not all(k in config for k in ['newbook_api_key', 'newbook_username', 'newbook_password', 'newbook_region']):
|
if not all(k in config for k in ['newbook_api_key', 'newbook_username', 'newbook_password', 'newbook_region']):
|
||||||
logger.error("Newbook credentials not configured for single-date refresh")
|
logger.error("Newbook credentials not configured for single-date refresh")
|
||||||
return
|
return
|
||||||
|
creds = {
|
||||||
|
'api_key': config['newbook_api_key'],
|
||||||
|
'username': config['newbook_username'],
|
||||||
|
'password': config['newbook_password'],
|
||||||
|
'region': config['newbook_region'],
|
||||||
|
}
|
||||||
|
|
||||||
vat_rate = Decimal(config.get('accommodation_vat_rate', '0.20'))
|
vat_rate = Decimal(config.get('accommodation_vat_rate', '0.20'))
|
||||||
|
|
||||||
|
|
@ -509,10 +519,10 @@ def _refresh_date_sync(rate_date: date):
|
||||||
included_categories = set(row.site_id for row in cat_result.fetchall())
|
included_categories = set(row.site_id for row in cat_result.fetchall())
|
||||||
|
|
||||||
client = NewbookRatesClient(
|
client = NewbookRatesClient(
|
||||||
api_key=config['newbook_api_key'],
|
api_key=creds['api_key'],
|
||||||
username=config['newbook_username'],
|
username=creds['username'],
|
||||||
password=config['newbook_password'],
|
password=creds['password'],
|
||||||
region=config['newbook_region'],
|
region=creds['region'],
|
||||||
vat_rate=vat_rate
|
vat_rate=vat_rate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -747,6 +747,15 @@ async def _test_newbook(db: AsyncSession):
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Central Settings service first, app-local config fallback
|
||||||
|
from services.central_settings import get_newbook_credentials
|
||||||
|
central = await get_newbook_credentials()
|
||||||
|
if central:
|
||||||
|
api_key = central["api_key"]
|
||||||
|
username = central["username"]
|
||||||
|
password = central["password"]
|
||||||
|
region = central["region"]
|
||||||
|
else:
|
||||||
api_key = await _get_config_value(db, "newbook_api_key")
|
api_key = await _get_config_value(db, "newbook_api_key")
|
||||||
username = await _get_config_value(db, "newbook_username")
|
username = await _get_config_value(db, "newbook_username")
|
||||||
password = await _get_config_value(db, "newbook_password")
|
password = await _get_config_value(db, "newbook_password")
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,9 @@ def run_bookings_data_sync(
|
||||||
return row.config_value
|
return row.config_value
|
||||||
return None
|
return None
|
||||||
|
|
||||||
creds = {
|
# Central Settings service first, app-local config fallback
|
||||||
|
from services.central_settings import get_newbook_credentials_sync
|
||||||
|
creds = get_newbook_credentials_sync() or {
|
||||||
'api_key': get_config('newbook_api_key'),
|
'api_key': get_config('newbook_api_key'),
|
||||||
'username': get_config('newbook_username'),
|
'username': get_config('newbook_username'),
|
||||||
'password': get_config('newbook_password'),
|
'password': get_config('newbook_password'),
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,15 @@ def get_config_value(db, key: str) -> Optional[str]:
|
||||||
|
|
||||||
|
|
||||||
def load_newbook_credentials(db) -> dict:
|
def load_newbook_credentials(db) -> dict:
|
||||||
"""Load Newbook API credentials from database config."""
|
"""
|
||||||
|
Load Newbook API credentials — central Settings service first (stack-wide
|
||||||
|
config), falling back to the app-local database config.
|
||||||
|
"""
|
||||||
|
from services.central_settings import get_newbook_credentials_sync
|
||||||
|
central = get_newbook_credentials_sync()
|
||||||
|
if central:
|
||||||
|
return central
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
def decrypt(value: str) -> str:
|
def decrypt(value: str) -> str:
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,11 @@ async def run_fetch_current_rates(horizon_days: int = 720, start_date: date = No
|
||||||
import base64
|
import base64
|
||||||
from services.newbook_rates_client import NewbookRatesClient
|
from services.newbook_rates_client import NewbookRatesClient
|
||||||
|
|
||||||
# Get credentials from config (decrypt encrypted values)
|
# Credentials: central Settings service first, app-local config fallback
|
||||||
|
from services.central_settings import get_newbook_credentials_sync
|
||||||
|
creds = get_newbook_credentials_sync()
|
||||||
|
|
||||||
|
if not creds:
|
||||||
config_result = db.execute(
|
config_result = db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT config_key, config_value, COALESCE(is_encrypted, false) as is_encrypted
|
SELECT config_key, config_value, COALESCE(is_encrypted, false) as is_encrypted
|
||||||
|
|
@ -247,12 +251,19 @@ async def run_fetch_current_rates(horizon_days: int = 720, start_date: date = No
|
||||||
logger.error("Newbook credentials not configured")
|
logger.error("Newbook credentials not configured")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
creds = {
|
||||||
|
'api_key': config['newbook_api_key'],
|
||||||
|
'username': config['newbook_username'],
|
||||||
|
'password': config['newbook_password'],
|
||||||
|
'region': config['newbook_region'],
|
||||||
|
}
|
||||||
|
|
||||||
# Create client
|
# Create client
|
||||||
client = NewbookRatesClient(
|
client = NewbookRatesClient(
|
||||||
api_key=config['newbook_api_key'],
|
api_key=creds['api_key'],
|
||||||
username=config['newbook_username'],
|
username=creds['username'],
|
||||||
password=config['newbook_password'],
|
password=creds['password'],
|
||||||
region=config['newbook_region'],
|
region=creds['region'],
|
||||||
vat_rate=Decimal(vat_rate_str)
|
vat_rate=Decimal(vat_rate_str)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
100
backend/services/central_settings.py
Normal file
100
backend/services/central_settings.py
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
"""
|
||||||
|
Client for the stack's central Settings service.
|
||||||
|
|
||||||
|
NewBook credentials are managed once in the Settings app (LXC 116) and
|
||||||
|
fetched live by every app — the same pattern as cashup / room-planner /
|
||||||
|
maintenance (see their lib/newbook.js). Falls back to None if the service
|
||||||
|
is unreachable so callers can fall back to app-local config.
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SETTINGS_URL = os.getenv("SETTINGS_URL", "")
|
||||||
|
SETTINGS_SECRET = os.getenv("SETTINGS_SECRET", "")
|
||||||
|
|
||||||
|
_CACHE_TTL = 60 # seconds — credentials change rarely; avoid hammering the service
|
||||||
|
_cache: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_integration(name: str) -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Fetch integration config (e.g. 'newbook') from the central Settings
|
||||||
|
service. Returns the config dict, or None if unavailable/unconfigured.
|
||||||
|
"""
|
||||||
|
if not SETTINGS_URL or not SETTINGS_SECRET:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cached = _cache.get(name)
|
||||||
|
if cached and (time.monotonic() - cached[0]) < _CACHE_TTL:
|
||||||
|
return cached[1]
|
||||||
|
|
||||||
|
url = f"{SETTINGS_URL}/settings/api/internal/integration/{name}"
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||||
|
resp = await client.get(
|
||||||
|
url, headers={"Authorization": f"Bearer {SETTINGS_SECRET}"}
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
_cache[name] = (time.monotonic(), data)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Central settings fetch failed for '{name}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_newbook(s: Optional[dict]) -> Optional[dict]:
|
||||||
|
if not s:
|
||||||
|
return None
|
||||||
|
creds = {
|
||||||
|
"api_key": s.get("api_key") or "",
|
||||||
|
"username": s.get("username") or "",
|
||||||
|
"password": s.get("password") or "",
|
||||||
|
"region": s.get("region") or "eu",
|
||||||
|
}
|
||||||
|
# Only usable if the essential fields are present
|
||||||
|
if not (creds["api_key"] and creds["username"] and creds["password"]):
|
||||||
|
return None
|
||||||
|
return creds
|
||||||
|
|
||||||
|
|
||||||
|
async def get_newbook_credentials() -> Optional[dict]:
|
||||||
|
"""
|
||||||
|
Returns {'api_key', 'username', 'password', 'region'} from central
|
||||||
|
settings, or None if not available (caller should fall back).
|
||||||
|
"""
|
||||||
|
return _extract_newbook(await get_integration("newbook"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_integration_sync(name: str) -> Optional[dict]:
|
||||||
|
"""Blocking variant of get_integration for sync job contexts."""
|
||||||
|
if not SETTINGS_URL or not SETTINGS_SECRET:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cached = _cache.get(name)
|
||||||
|
if cached and (time.monotonic() - cached[0]) < _CACHE_TTL:
|
||||||
|
return cached[1]
|
||||||
|
|
||||||
|
url = f"{SETTINGS_URL}/settings/api/internal/integration/{name}"
|
||||||
|
try:
|
||||||
|
resp = httpx.get(
|
||||||
|
url, headers={"Authorization": f"Bearer {SETTINGS_SECRET}"}, timeout=5.0
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
_cache[name] = (time.monotonic(), data)
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Central settings fetch failed for '{name}': {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_newbook_credentials_sync() -> Optional[dict]:
|
||||||
|
"""Blocking variant of get_newbook_credentials for sync job contexts."""
|
||||||
|
return _extract_newbook(get_integration_sync("newbook"))
|
||||||
|
|
@ -48,7 +48,17 @@ class NewbookClient:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_db(cls, db):
|
async def from_db(cls, db):
|
||||||
"""Create client with credentials from database"""
|
"""
|
||||||
|
Create client with credentials from the central Settings service
|
||||||
|
(stack-wide NewBook config), falling back to the app-local
|
||||||
|
system_config table for standalone/dev use.
|
||||||
|
"""
|
||||||
|
from services.central_settings import get_newbook_credentials
|
||||||
|
|
||||||
|
central = await get_newbook_credentials()
|
||||||
|
if central:
|
||||||
|
return cls(**central)
|
||||||
|
|
||||||
from api.config import _get_config_value
|
from api.config import _get_config_value
|
||||||
|
|
||||||
api_key = await _get_config_value(db, "newbook_api_key")
|
api_key = await _get_config_value(db, "newbook_api_key")
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,14 @@ class NewbookRatesClient:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def from_db(cls, db):
|
async def from_db(cls, db):
|
||||||
"""Create client with credentials and VAT rate from database"""
|
"""
|
||||||
|
Create client with credentials from the central Settings service
|
||||||
|
(stack-wide NewBook config), falling back to the app-local
|
||||||
|
system_config table. VAT rate stays app-local either way.
|
||||||
|
"""
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
from services.central_settings import get_newbook_credentials
|
||||||
|
|
||||||
# Get credentials from config
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("SELECT config_key, config_value FROM system_config WHERE config_key IN ('newbook_api_key', 'newbook_username', 'newbook_password', 'newbook_region', 'accommodation_vat_rate')")
|
text("SELECT config_key, config_value FROM system_config WHERE config_key IN ('newbook_api_key', 'newbook_username', 'newbook_password', 'newbook_region', 'accommodation_vat_rate')")
|
||||||
)
|
)
|
||||||
|
|
@ -63,6 +67,10 @@ class NewbookRatesClient:
|
||||||
|
|
||||||
vat_rate = Decimal(config.get('accommodation_vat_rate', '0.20'))
|
vat_rate = Decimal(config.get('accommodation_vat_rate', '0.20'))
|
||||||
|
|
||||||
|
central = await get_newbook_credentials()
|
||||||
|
if central:
|
||||||
|
return cls(**central, vat_rate=vat_rate)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
api_key=config.get('newbook_api_key'),
|
api_key=config.get('newbook_api_key'),
|
||||||
username=config.get('newbook_username'),
|
username=config.get('newbook_username'),
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,8 @@ services:
|
||||||
- DATABASE_URL=${DATABASE_URL}
|
- DATABASE_URL=${DATABASE_URL}
|
||||||
- CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
- CENTRAL_AUTH_SECRET=${CENTRAL_AUTH_SECRET}
|
||||||
- APP_SLUG=forecasting
|
- APP_SLUG=forecasting
|
||||||
- NEWBOOK_API_KEY=${NEWBOOK_API_KEY:-}
|
- SETTINGS_URL=${SETTINGS_URL:-}
|
||||||
- NEWBOOK_USERNAME=${NEWBOOK_USERNAME:-}
|
- SETTINGS_SECRET=${SETTINGS_SECRET:-}
|
||||||
- NEWBOOK_PASSWORD=${NEWBOOK_PASSWORD:-}
|
|
||||||
- NEWBOOK_REGION=${NEWBOOK_REGION:-AU}
|
|
||||||
- RESOS_API_KEY=${RESOS_API_KEY:-}
|
- RESOS_API_KEY=${RESOS_API_KEY:-}
|
||||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||||
volumes:
|
volumes:
|
||||||
|
|
|
||||||
|
|
@ -2024,61 +2024,9 @@ const CurrentRatesDataSyncSection: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const NewbookPage: 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 [testStatus, setTestStatus] = useState<'idle' | 'testing' | 'success' | 'error'>('idle')
|
||||||
const [testMessage, setTestMessage] = useState('')
|
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 () => {
|
const handleTestConnection = async () => {
|
||||||
setTestStatus('testing')
|
setTestStatus('testing')
|
||||||
setTestMessage('')
|
setTestMessage('')
|
||||||
|
|
@ -2104,93 +2052,18 @@ const NewbookPage: React.FC = () => {
|
||||||
}, 5000)
|
}, 5000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div style={styles.section}>
|
|
||||||
<div style={styles.loading}>Loading settings...</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>Newbook Settings</h2>
|
<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}>
|
<div style={styles.infoBox}>
|
||||||
{/* Left side - API Configuration */}
|
Newbook API credentials are managed centrally in the stack <strong>Settings</strong> app
|
||||||
<div style={styles.apiConfigLeft}>
|
(Integrations → NewBook) and shared by all apps. Use the button below to verify this
|
||||||
<h3 style={styles.subsectionTitle}>API Configuration</h3>
|
app can reach Newbook with those credentials.
|
||||||
|
|
||||||
<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>
|
</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}>
|
<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
|
<button
|
||||||
onClick={handleTestConnection}
|
onClick={handleTestConnection}
|
||||||
disabled={testStatus === 'testing'}
|
disabled={testStatus === 'testing'}
|
||||||
|
|
@ -2205,44 +2078,11 @@ const NewbookPage: React.FC = () => {
|
||||||
...styles.statusMessage,
|
...styles.statusMessage,
|
||||||
background: testStatus === 'success' ? colors.successBg : colors.errorBg,
|
background: testStatus === 'success' ? colors.successBg : colors.errorBg,
|
||||||
color: testStatus === 'success' ? colors.success : colors.error,
|
color: testStatus === 'success' ? colors.success : colors.error,
|
||||||
|
marginTop: spacing.md,
|
||||||
}}>
|
}}>
|
||||||
{testMessage}
|
{testMessage}
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
<div style={styles.divider} />
|
<div style={styles.divider} />
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue