116 lines
4.3 KiB
JavaScript
116 lines
4.3 KiB
JavaScript
/**
|
|
* Referee impact signal.
|
|
*
|
|
* Game-day ref assignments live in `game_ref_assignments` (migration 017).
|
|
* Per-referee tendencies live in `ref_profiles`, populated by the
|
|
* Sports-Reference scraper (scripts/scrape-sports-reference.js).
|
|
*
|
|
* Crew impact is computed by averaging the three refs' profiles. If any
|
|
* profile is missing we still return a partial impact (averaging only the
|
|
* available refs) — the feature cache decides whether to surface the
|
|
* feature or omit it based on coverage.
|
|
*/
|
|
|
|
const { getSupabaseServiceClient } = require('../../utils/supabase');
|
|
|
|
const LOOPBACK_IPS = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
|
|
|
async function getRefAssignment(gameId) {
|
|
const supabase = getSupabaseServiceClient();
|
|
const { data, error } = await supabase
|
|
.from('game_ref_assignments')
|
|
.select('ref1_name, ref2_name, ref3_name, ref_crew_avg_fouls, ref_crew_pace_impact')
|
|
.eq('game_id', gameId)
|
|
.maybeSingle();
|
|
if (error) {
|
|
console.warn('[refSignals] assignment lookup failed:', error.message);
|
|
return null;
|
|
}
|
|
return data || null;
|
|
}
|
|
|
|
async function getRefProfiles(refNames) {
|
|
const named = refNames.filter(Boolean);
|
|
if (named.length === 0) return [];
|
|
const supabase = getSupabaseServiceClient();
|
|
const { data, error } = await supabase
|
|
.from('ref_profiles')
|
|
.select('ref_name, avg_fouls_per_game, avg_free_throws_per_game, pace_impact, home_whistle_bias')
|
|
.in('ref_name', named);
|
|
if (error) {
|
|
console.warn('[refSignals] profile lookup failed:', error.message);
|
|
return [];
|
|
}
|
|
return data || [];
|
|
}
|
|
|
|
function average(values) {
|
|
const clean = values.filter((v) => Number.isFinite(v));
|
|
if (clean.length === 0) return null;
|
|
return clean.reduce((a, b) => a + b, 0) / clean.length;
|
|
}
|
|
|
|
async function getRefImpact(gameId) {
|
|
const assignment = await getRefAssignment(gameId);
|
|
if (!assignment) return null;
|
|
const crew = [assignment.ref1_name, assignment.ref2_name, assignment.ref3_name].filter(Boolean);
|
|
if (crew.length === 0) return null;
|
|
|
|
// If precomputed crew values exist on the assignment row (scraper wrote
|
|
// them), prefer those — they were derived from the same profiles but
|
|
// baked at assignment time. Note: Number(null) === 0 is finite, so guard
|
|
// explicitly against null/undefined before going through Number().
|
|
const hasFouls = assignment.ref_crew_avg_fouls != null && Number.isFinite(Number(assignment.ref_crew_avg_fouls));
|
|
const hasPace = assignment.ref_crew_pace_impact != null && Number.isFinite(Number(assignment.ref_crew_pace_impact));
|
|
if (hasFouls || hasPace) {
|
|
return {
|
|
crew,
|
|
avg_fouls: assignment.ref_crew_avg_fouls,
|
|
pace_impact: assignment.ref_crew_pace_impact,
|
|
foul_adjustment: assignment.ref_crew_avg_fouls,
|
|
home_bias: null,
|
|
profilesUsed: crew.length,
|
|
};
|
|
}
|
|
|
|
const profiles = await getRefProfiles(crew);
|
|
return {
|
|
crew,
|
|
avg_fouls: average(profiles.map((p) => p.avg_fouls_per_game)),
|
|
pace_impact: average(profiles.map((p) => p.pace_impact)),
|
|
foul_adjustment: average(profiles.map((p) => p.avg_free_throws_per_game)),
|
|
home_bias: average(profiles.map((p) => p.home_whistle_bias)),
|
|
profilesUsed: profiles.length,
|
|
};
|
|
}
|
|
|
|
// Manual entry endpoint helper — the route module (not built here) calls
|
|
// this when ops POSTs an assignment.
|
|
async function setRefAssignment(gameId, sport, gameDate, refs) {
|
|
const supabase = getSupabaseServiceClient();
|
|
// Pull profiles synchronously to precompute crew impact at insert time so
|
|
// downstream reads don't need a join.
|
|
const profiles = await getRefProfiles(refs);
|
|
const avgFouls = average(profiles.map((p) => p.avg_fouls_per_game));
|
|
const paceImpact = average(profiles.map((p) => p.pace_impact));
|
|
const { error } = await supabase
|
|
.from('game_ref_assignments')
|
|
.upsert({
|
|
game_id: gameId,
|
|
sport,
|
|
game_date: gameDate,
|
|
ref1_name: refs[0] || null,
|
|
ref2_name: refs[1] || null,
|
|
ref3_name: refs[2] || null,
|
|
ref_crew_avg_fouls: avgFouls,
|
|
ref_crew_pace_impact: paceImpact,
|
|
}, { onConflict: 'game_id' });
|
|
if (error) {
|
|
console.warn('[refSignals] assignment upsert failed:', error.message);
|
|
return { ok: false, error: error.message };
|
|
}
|
|
return { ok: true, avg_fouls: avgFouls, pace_impact: paceImpact };
|
|
}
|
|
|
|
module.exports = { getRefImpact, getRefAssignment, getRefProfiles, setRefAssignment, LOOPBACK_IPS };
|