Add configurable 30-day NewBook rate rescrape on 2/4/6/12h intervals
Intraday rescrape jobs are distributed evenly between the main nightly run (05:20) and cover only the next 30 days — lightweight complement to the full 720-day nightly sweep. Interval is configurable from the Newbook tab in Settings and takes effect immediately without a container restart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7d0b7d2d3b
commit
078cb47b16
3 changed files with 238 additions and 1 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { Save, RefreshCw, Database, Clock, BedDouble, ChevronUp, ChevronDown, Network, ShieldCheck, AlertTriangle } from 'lucide-react'
|
||||
import { Save, RefreshCw, Database, Clock, BedDouble, ChevronUp, ChevronDown, Network, ShieldCheck, AlertTriangle, Timer } from 'lucide-react'
|
||||
import api from '../api'
|
||||
|
||||
const TABS = [
|
||||
|
|
@ -664,6 +664,109 @@ function NewbookTab({ config, isLoading, onSave, onSyncNow, saving, syncing }: N
|
|||
</div>
|
||||
|
||||
<RoomCategoriesCard />
|
||||
<RescrapeCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── 30-day Rescrape Schedule ─────────────────────────────────────────────────
|
||||
|
||||
interface RescrapeSchedule {
|
||||
interval_hours: number
|
||||
base_time: string
|
||||
rescrape_times: string[]
|
||||
}
|
||||
|
||||
function RescrapeCard() {
|
||||
const qc = useQueryClient()
|
||||
const [interval, setInterval] = useState<number>(0)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
|
||||
const { data, isLoading } = useQuery<RescrapeSchedule>({
|
||||
queryKey: ['rescrape-schedule'],
|
||||
queryFn: () => api.get('/bookability/config/rescrape-schedule').then(r => r.data),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (data && !loaded) {
|
||||
setInterval(data.interval_hours)
|
||||
setLoaded(true)
|
||||
}
|
||||
}, [data, loaded])
|
||||
|
||||
const save = useMutation({
|
||||
mutationFn: () =>
|
||||
api.post('/bookability/config/rescrape-interval', { interval_hours: interval }).then(r => r.data as RescrapeSchedule),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ['rescrape-schedule'] }),
|
||||
})
|
||||
|
||||
const previewTimes = (data && loaded)
|
||||
? (() => {
|
||||
const baseTime = data.base_time
|
||||
if (interval === 0) return []
|
||||
const [bh, bm] = baseTime.split(':').map(Number)
|
||||
const times: string[] = []
|
||||
for (let offset = interval; offset < 24; offset += interval) {
|
||||
const h = (bh + offset) % 24
|
||||
times.push(`${String(h).padStart(2, '0')}:${String(bm).padStart(2, '0')}`)
|
||||
}
|
||||
return times
|
||||
})()
|
||||
: data?.rescrape_times ?? []
|
||||
|
||||
if (isLoading) return null
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<Timer size={16} strokeWidth={1.75} />
|
||||
30-day Rate Rescrape
|
||||
</span>
|
||||
<span className={`badge ${interval > 0 ? 'badge-success' : 'badge-neutral'}`}>
|
||||
{interval > 0 ? `Every ${interval}h` : 'Disabled'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="card-body" style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-mid)', margin: 0 }}>
|
||||
Re-fetches the next 30 days of NewBook rates throughout the day, between the main
|
||||
nightly run at <strong>{data?.base_time ?? '05:20'}</strong>. Useful for keeping
|
||||
Bookability fresh on days when tariff availability changes.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<select
|
||||
style={{ width: 200 }}
|
||||
value={interval}
|
||||
onChange={e => setInterval(Number(e.target.value))}
|
||||
>
|
||||
<option value={0}>Disabled</option>
|
||||
<option value={2}>Every 2 hours</option>
|
||||
<option value={4}>Every 4 hours</option>
|
||||
<option value={6}>Every 6 hours</option>
|
||||
<option value={12}>Every 12 hours</option>
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={save.isPending}
|
||||
onClick={() => save.mutate()}
|
||||
>
|
||||
<Save size={13} strokeWidth={1.75} />
|
||||
{save.isPending ? 'Saving…' : 'Save'}
|
||||
</button>
|
||||
{save.isSuccess && (
|
||||
<span style={{ fontSize: 12, color: 'var(--success)' }}>✓ Saved</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{previewTimes.length > 0 && (
|
||||
<div style={{ fontSize: 12, color: 'var(--text-mid)' }}>
|
||||
<span style={{ fontWeight: 600, marginRight: 6 }}>Rescrape runs at:</span>
|
||||
{previewTimes.join(' · ')}
|
||||
<span style={{ marginLeft: 6 }}>(in addition to the {data?.base_time} main run)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue