diff --git a/btc-UI/src/app/service/stopbet.service.ts b/btc-UI/src/app/service/stopbet.service.ts index 749ee52..5cec3cf 100644 --- a/btc-UI/src/app/service/stopbet.service.ts +++ b/btc-UI/src/app/service/stopbet.service.ts @@ -1,16 +1,16 @@ -import { Injectable } from '@angular/core'; +import { Injectable, OnDestroy } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { NgZone } from '@angular/core'; @Injectable({ providedIn: 'root' }) -export class StopbetService { +export class StopbetService implements OnDestroy { private stopbetStatuses: Map = new Map(); // raceNum => 'Y'|'N'|'S' private stopbetStatusesSubject = new BehaviorSubject>(this.stopbetStatuses); private currentVenue: string = ''; private currentDate: string = ''; private eventSource?: EventSource; - private readonly STORAGE_KEY = 'stopbetStatuses'; + private readonly STORAGE_KEY = 'Stopbet'; // single key that persists across refreshes constructor(private http: HttpClient, private zone: NgZone) {} @@ -20,48 +20,55 @@ export class StopbetService { } // Initialize stopbet for a venue and date + // NOTE: we DO NOT clear localStorage here — storage is preserved across refreshes. initialize(venue: string, date: string) { const newVenue = venue ? venue.toUpperCase() : ''; const newDate = date || this.getTodayDate(); - // Only clear if venue and date are valid and different if (newVenue && newDate && (this.currentVenue !== newVenue || this.currentDate !== newDate)) { console.log( - `[STOPBET] Venue or date changed (from ${this.currentVenue}:${this.currentDate} to ${newVenue}:${newDate}), clearing stopbetStatuses` + `[STOPBET] Venue/date changed (from ${this.currentVenue}:${this.currentDate} to ${newVenue}:${newDate}). Keeping Stopbet storage intact until explicit logout.` ); - this.stopbetStatuses.clear(); - localStorage.removeItem(this.STORAGE_KEY); - this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); } else { console.log( - `[STOPBET] No clear needed: newVenue=${newVenue}, newDate=${newDate}, currentVenue=${this.currentVenue}, currentDate=${this.currentDate}` + `[STOPBET] initialize called: newVenue=${newVenue}, newDate=${newDate}, currentVenue=${this.currentVenue}, currentDate=${this.currentDate}` ); } this.currentVenue = newVenue; this.currentDate = newDate; - // Load from localStorage first + // Load from localStorage (Stopbet) if present this.loadFromLocalStorage(); - // Fetch from server and setup SSE + // Fetch from server and setup SSE (these will call setStatus which persists) this.fetchInitialStopbets(); this.setupSSE(); } // Public method to set race status setStatus(raceNum: number, status: string) { + if (!Number.isFinite(raceNum) || raceNum <= 0) { + console.warn(`[STOPBET] Ignoring invalid raceNum: ${raceNum}`); + return; + } + const normalizedStatus = String(status).toUpperCase(); + if (!['Y', 'N', 'S'].includes(normalizedStatus)) { + console.warn(`[STOPBET] Ignoring invalid status for race ${raceNum}: ${status}`); + return; + } + const oldStatus = this.stopbetStatuses.get(raceNum); - if (oldStatus === status) { - console.log(`[STOPBET] No change for race ${raceNum}: status remains ${status}`); + if (oldStatus === normalizedStatus) { + console.log(`[STOPBET] No change for race ${raceNum}: status remains ${normalizedStatus}`); return; } // Set the specified race status - this.stopbetStatuses.set(raceNum, status); + this.stopbetStatuses.set(raceNum, normalizedStatus); // If stopping this race, auto-disable previous races - if (status === 'Y' || status === 'S') { + if (normalizedStatus === 'Y' || normalizedStatus === 'S') { for (let prevRace = 1; prevRace < raceNum; prevRace++) { const prevStatus = this.stopbetStatuses.get(prevRace); if (prevStatus === 'N' || prevStatus === undefined) { @@ -76,65 +83,105 @@ export class StopbetService { this.saveToLocalStorage(); } + // Load Stopbet entry from localStorage if it is parseable. + // We will only apply it if the stored venue/date match currentVenue/currentDate. private loadFromLocalStorage() { const cached = localStorage.getItem(this.STORAGE_KEY); - if (cached) { - try { - const parsed = JSON.parse(cached); - if (parsed.venue === this.currentVenue && parsed.date === this.currentDate) { - this.stopbetStatuses.clear(); - for (const [race, status] of Object.entries(parsed.statuses)) { - const raceNum = parseInt(race, 10); - if (!isNaN(raceNum) && ['Y', 'N', 'S'].includes(status as string)) { - this.stopbetStatuses.set(raceNum, status as string); - } - } - console.log('[STOPBET] Loaded statuses from localStorage:', this.stopbetStatuses); - this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); - } else { - console.log( - `[STOPBET] localStorage data invalid for venue=${this.currentVenue}, date=${this.currentDate}:`, - parsed - ); - } - } catch (err) { - console.error('[STOPBET] Failed to parse localStorage:', err); - localStorage.removeItem(this.STORAGE_KEY); + if (!cached) { + console.log('[STOPBET] No Stopbet entry found in localStorage'); + return; + } + + try { + const parsed = JSON.parse(cached); + if (!parsed || typeof parsed !== 'object') { + console.warn('[STOPBET] Stopbet entry has unexpected shape, ignoring.'); + return; + } + + // If stored venue/date match current ones, load statuses into memory + if (parsed.venue === this.currentVenue && parsed.date === this.currentDate && parsed.statuses) { + this.stopbetStatuses.clear(); + for (const [race, status] of Object.entries(parsed.statuses)) { + const raceNum = parseInt(race, 10); + const s = String(status).toUpperCase(); + if (!isNaN(raceNum) && ['Y', 'N', 'S'].includes(s)) { + this.stopbetStatuses.set(raceNum, s); + } + } + console.log('[STOPBET] Loaded Stopbet from localStorage for current venue/date:', { + venue: parsed.venue, + date: parsed.date, + statuses: parsed.statuses, + }); + this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); + } else { + // If the stored Stopbet is for a different venue/date, keep it in storage (do not remove). + console.log( + `[STOPBET] Stopbet in storage is for ${parsed.venue}:${parsed.date} — not loading into current ${this.currentVenue}:${this.currentDate}.` + ); + } + } catch (err) { + console.error('[STOPBET] Failed to parse Stopbet from localStorage:', err); + // If corrupted, remove it to allow fresh writes next time + try { + localStorage.removeItem(this.STORAGE_KEY); + console.warn('[STOPBET] Removed corrupted Stopbet entry from localStorage.'); + } catch (e) { + console.error('[STOPBET] Failed to remove corrupted Stopbet entry:', e); } - } else { - console.log('[STOPBET] No stopbetStatuses found in localStorage'); } } + // Save the current statuses under Stopbet key (includes venue/date and a timestamp) private saveToLocalStorage() { try { const data = { venue: this.currentVenue, date: this.currentDate, - statuses: Object.fromEntries(this.stopbetStatuses), + statuses: Object.fromEntries(this.stopbetStatuses), // { "1":"Y", ... } + updatedAt: new Date().toISOString(), }; localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data)); - console.log('[STOPBET] Saved statuses to localStorage:', data); + console.log('[STOPBET] Saved Stopbet to localStorage:', data); } catch (err) { - console.error('[STOPBET] Failed to save to localStorage:', err); + console.error('[STOPBET] Failed to save Stopbet to localStorage:', err); } } + // Public helper to clear the Stopbet entry and reset in-memory statuses (call on logout) + public clearStopbetStorage() { + try { + localStorage.removeItem(this.STORAGE_KEY); + this.stopbetStatuses.clear(); + this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); + console.log('[STOPBET] Cleared Stopbet storage and in-memory statuses (logout reset)'); + } catch (err) { + console.error('[STOPBET] Failed to clear Stopbet storage:', err); + } + } + + // Fetch initial statuses from backend and apply them private fetchInitialStopbets() { + if (!this.currentVenue || !this.currentDate) { + console.log('[STOPBET] fetchInitialStopbets skipped: venue/date not set'); + return; + } + this.http .get(`http://localhost:8080/stopbet/raw?venue=${this.currentVenue}&date=${this.currentDate}`) .subscribe({ next: (res: any) => { - if (res.ok && res.data) { + if (res && res.ok && res.data) { const key = `${this.currentVenue}:${this.currentDate}`; const meeting = res.data[key] || {}; - this.stopbetStatuses.clear(); + // Use setStatus to ensure auto-stop logic and persistence run for (const race in meeting) { const raceNum = parseInt(race, 10); const status = meeting[race]; this.setStatus(raceNum, status); } - console.log('[STOPBET] Fetched initial statuses:', this.stopbetStatuses); + console.log('[STOPBET] Fetched initial statuses and applied them.'); this.stopbetStatusesSubject.next(new Map(this.stopbetStatuses)); this.saveToLocalStorage(); } else { @@ -147,16 +194,28 @@ export class StopbetService { }); } + // Setup Server-Sent Events stream for real-time updates private setupSSE() { if (this.eventSource) { - this.eventSource.close(); + try { + this.eventSource.close(); + } catch (e) { + /* ignore close errors */ + } } - this.eventSource = new EventSource('http://localhost:8080/stopbet/stream'); + + try { + this.eventSource = new EventSource('http://localhost:8080/stopbet/stream'); + } catch (err) { + console.error('[STOPBET] Failed to create EventSource:', err); + return; + } + this.eventSource.onmessage = (event) => { this.zone.run(() => { try { const data = JSON.parse(event.data); - if (data.type === 'stopbet' && data.venue === this.currentVenue && data.date === this.currentDate) { + if (data && data.type === 'stopbet' && data.venue === this.currentVenue && data.date === this.currentDate) { const raceNum = parseInt(data.race, 10); this.setStatus(raceNum, data.status); console.log('[STOPBET] SSE updated race', raceNum, 'to', data.status); @@ -166,17 +225,23 @@ export class StopbetService { } }); }; + this.eventSource.onerror = (err) => { console.error('[STOPBET] SSE error:', err); }; } + // Returns true if betting is open for the given race isOpen(race: number): boolean { const status = this.stopbetStatuses.get(race); return status === 'N' || status === undefined; } + // Find open race starting from 'start' up to maxRaces, wrapping around getOpenRaceStartingFrom(start: number, maxRaces: number): number { + if (!Number.isFinite(start) || !Number.isFinite(maxRaces) || maxRaces <= 0) { + return start; + } for (let r = start; r <= maxRaces; r++) { if (this.isOpen(r)) return r; } @@ -196,7 +261,11 @@ export class StopbetService { ngOnDestroy() { if (this.eventSource) { - this.eventSource.close(); + try { + this.eventSource.close(); + } catch (e) { + /* ignore close errors */ + } } } -} \ No newline at end of file +}