Auto-recheck parity after scrape + manual Recheck button + faster badge

Parity logic:
- Extract _fetch_latest_alerts() and _upsert_alerts() helpers so the alert
  upsert loop is no longer duplicated between the daily job and per-date runs
- Add run_parity_check_for_date(date) which runs the full comparison +
  alert upsert for a single date

Scraper integration:
- _safe_parity_recheck(date) wrapper (never raises) called after each
  successful date in both search-results and hotel-page workers; hotel-page
  mode waits until all hotels for the date are done before rechecking

API:
- POST /competitors/parity/check-date?rate_date=YYYY-MM-DD for manual recheck

Frontend:
- Recheck button on every parity alert row (all statuses); invalidates
  alerts list and badge count on success
- parity-alert-count badge polls every 15s (was 60s) so new alerts from
  the scheduled job or post-scrape rechecks appear quickly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-15 09:33:33 +00:00
parent 8a80c66a3b
commit cab1a39503
5 changed files with 146 additions and 66 deletions

View file

@ -23,7 +23,7 @@ export default function Layout({ children }: { children: ReactNode }) {
const { data: alertCount } = useQuery<number>({
queryKey: ['parity-alert-count'],
queryFn: () => api.get('/competitors/parity/alerts?status=active').then(r => r.data.length),
refetchInterval: 60_000,
refetchInterval: 15_000,
enabled: can(user, 'view_competitors'),
})

View file

@ -843,6 +843,15 @@ const ParityAlertsTab: React.FC = () => {
},
})
const recheckMutation = useMutation({
mutationFn: async (rateDate: string) =>
(await api.post(`/competitors/parity/check-date?rate_date=${rateDate}`)).data,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['parity-alerts'] })
queryClient.invalidateQueries({ queryKey: ['parity-alert-count'] })
},
})
const markupUnit = parityConfig?.['parity_markup_unit'] ?? 'pct'
const toleranceUnit = parityConfig?.['parity_tolerance_unit'] ?? 'pct'
const markupVal = parityConfig?.['parity_markup_value'] ?? parityConfig?.['parity_expected_markup_pct'] ?? '0'
@ -945,19 +954,29 @@ const ParityAlertsTab: React.FC = () => {
{a.alert_status}
</span>
</td>
<td style={tdStyle}>
{a.alert_status === 'active' && (
<td style={mergeStyles(tdStyle, { whiteSpace: 'nowrap' })}>
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
{a.alert_status === 'active' && (
<button
style={buttonStyle('outline', 'small')}
disabled={ackMutation.isPending || recheckMutation.isPending}
onClick={() => ackMutation.mutate(a.id)}
>
Acknowledge
</button>
)}
<button
style={buttonStyle('outline', 'small')}
disabled={ackMutation.isPending}
onClick={() => ackMutation.mutate(a.id)}
disabled={recheckMutation.isPending || ackMutation.isPending}
onClick={() => recheckMutation.mutate(a.rate_date)}
title="Re-run parity check using the latest scraped rates for this date"
>
Acknowledge
Recheck
</button>
)}
{a.alert_status === 'acknowledged' && a.acknowledged_by && (
<span style={{ fontSize: 11, color: 'var(--text-mid)' }}>by {a.acknowledged_by}</span>
)}
{a.alert_status === 'acknowledged' && a.acknowledged_by && (
<span style={{ fontSize: 11, color: 'var(--text-mid)' }}>by {a.acknowledged_by}</span>
)}
</div>
</td>
</tr>
))}