Fix Plotly £ format warning and add scraper backend selector to Settings
- MarketView: split tickformat '£,.0f' into tickprefix+'£' + tickformat ',.0f' (Plotly d3 format doesn't accept £ prefix inline) - Settings System tab: add editable dropdown for booking_scraper_backend (hotel page vs search results) with description of the tradeoff; backend was previously read-only in the table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f2e11bf585
commit
121976b3db
2 changed files with 40 additions and 5 deletions
|
|
@ -1252,7 +1252,7 @@ const RateHistoryModalComponent: React.FC<{ modal: RateHistoryModal; onClose: ()
|
||||||
height: 300,
|
height: 300,
|
||||||
legend: { orientation: 'h', y: -0.25 },
|
legend: { orientation: 'h', y: -0.25 },
|
||||||
xaxis: { title: { text: '' }, tickformat: '%d %b %H:%M', type: 'date' },
|
xaxis: { title: { text: '' }, tickformat: '%d %b %H:%M', type: 'date' },
|
||||||
yaxis: { title: { text: 'Rate (£)' }, tickformat: '£,.0f' },
|
yaxis: { title: { text: 'Rate (£)' }, tickprefix: '£', tickformat: ',.0f' },
|
||||||
plot_bgcolor: 'var(--body-bg)',
|
plot_bgcolor: 'var(--body-bg)',
|
||||||
paper_bgcolor: 'var(--card-bg)',
|
paper_bgcolor: 'var(--card-bg)',
|
||||||
font: { family: 'inherit', size: 11, color: 'var(--text-dark)' },
|
font: { family: 'inherit', size: 11, color: 'var(--text-dark)' },
|
||||||
|
|
|
||||||
|
|
@ -798,13 +798,22 @@ function RoomCategoriesCard() {
|
||||||
// ─── System Tab ───────────────────────────────────────────────────────────────
|
// ─── System Tab ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SystemTab({ config, isLoading }: { config: SystemConfig | undefined; isLoading: boolean }) {
|
function SystemTab({ config, isLoading }: { config: SystemConfig | undefined; isLoading: boolean }) {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
const saveMutation = useMutation({
|
||||||
|
mutationFn: (payload: { key: string; value: string }) =>
|
||||||
|
api.post('/competitors/config/system', payload),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['system-config'] }),
|
||||||
|
})
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="loading-state"><div className="spinner" /> Loading…</div>
|
return <div className="loading-state"><div className="spinner" /> Loading…</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backend = config?.['booking_scraper_backend'] ?? 'playwright_local'
|
||||||
|
|
||||||
const displayKeys = [
|
const displayKeys = [
|
||||||
'booking_scraper_enabled',
|
'booking_scraper_enabled',
|
||||||
'booking_scraper_backend',
|
|
||||||
'booking_scraper_daily_time',
|
'booking_scraper_daily_time',
|
||||||
'booking_proxy_enabled',
|
'booking_proxy_enabled',
|
||||||
'booking_proxy_host',
|
'booking_proxy_host',
|
||||||
|
|
@ -815,7 +824,33 @@ function SystemTab({ config, isLoading }: { config: SystemConfig | undefined; is
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ maxWidth: 700 }}>
|
<div style={{ maxWidth: 700, display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">Scraper Backend</div>
|
||||||
|
<div className="card-body" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--text-mid)', margin: 0 }}>
|
||||||
|
Controls how Booking.com rates are fetched. <strong>Hotel page</strong> scrapes each
|
||||||
|
competitor's own property page — captures all room types and rate plan variants (meal
|
||||||
|
plan, cancellation policy, availability). <strong>Search results</strong> scrapes the
|
||||||
|
location search page — faster but only returns the single best-available rate per hotel
|
||||||
|
with no room-type breakdown.
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||||
|
<select
|
||||||
|
style={{ width: 260 }}
|
||||||
|
value={backend}
|
||||||
|
onChange={e => saveMutation.mutate({ key: 'booking_scraper_backend', value: e.target.value })}
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
>
|
||||||
|
<option value="playwright_hotel_page">Hotel page (all rooms + rate plans)</option>
|
||||||
|
<option value="playwright_local">Search results (best available only)</option>
|
||||||
|
</select>
|
||||||
|
{saveMutation.isPending && <span style={{ fontSize: 12, color: 'var(--text-mid)' }}>Saving…</span>}
|
||||||
|
{saveMutation.isSuccess && <span style={{ fontSize: 12, color: 'var(--success)' }}>✓ Saved</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">System Configuration</div>
|
<div className="card-header">System Configuration</div>
|
||||||
<div className="table-wrap">
|
<div className="table-wrap">
|
||||||
|
|
@ -842,7 +877,7 @@ function SystemTab({ config, isLoading }: { config: SystemConfig | undefined; is
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: 16, fontSize: 12, color: 'var(--text-mid)' }}>
|
<div style={{ fontSize: 12, color: 'var(--text-mid)' }}>
|
||||||
To configure the Booking.com scraper location and hotel tiers, use the Settings tab inside{' '}
|
To configure the Booking.com scraper location and hotel tiers, use the Settings tab inside{' '}
|
||||||
<a href="/rates/market" style={{ color: 'var(--gold)' }}>Market View</a>.
|
<a href="/rates/market" style={{ color: 'var(--gold)' }}>Market View</a>.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue