diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index c04ee5f..8feb8a2 100644 Binary files a/backend/__pycache__/main.cpython-311.pyc and b/backend/__pycache__/main.cpython-311.pyc differ diff --git a/backend/main.py b/backend/main.py index 05d027a..ef2ee0d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -112,15 +112,12 @@ def run_scrape_worker(facility_ids: List[int]): """ print(f"🔄 STARTER BAKGRUNNSSKRAPING FOR FØLGENDE IDER: {facility_ids}") - # Her kjører vi skraping-scriptet ditt via et system-kall (subprocess) - # Dette er den tryggeste måten å starte et annet script på uten å forstyrre API-et. try: - # Konverterer listen med IDer til en streng som vi kan sende som argument ids_arg = ",".join(map(str, facility_ids)) - # Vi antar at scrape_status.py ligger i samme mappe som main.py - # Slett /dev/null hvis du vil ha logg-utskrifter i terminalen. - command = f"python scrape_status.py --ids {ids_arg} > /dev/null 2>&1" + # NYTT: Bruker "python -u" for LIVE logging, og fjerner "> /dev/null 2>&1" + command = f"python -u scrape_status.py --ids {ids_arg}" + subprocess.run(command, shell=True, check=True) print(f"✅ BAKGRUNNSSKRAPING FULLFØRT FOR IDER: {facility_ids}") diff --git a/backend/scrape_status.py b/backend/scrape_status.py index 58ff8e9..7853eb0 100644 --- a/backend/scrape_status.py +++ b/backend/scrape_status.py @@ -23,22 +23,18 @@ DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_pass # ========================================== # KONFIGURERER GEMINI AI (NY SDK) # ========================================== -# Den nye pakken henter automatisk GEMINI_API_KEY fra .env-filen din client = genai.Client() async def ask_llm_status(text, course_name, is_single_course, ai_instruction=None): """Sender teksten til Gemini og ber om ett enkelt status-ord tilbake.""" - # 1. Dynamisk instruks basert på antall baner if is_single_course: bane_instruks = "Finn den generelle banestatusen for dette golfanlegget. Se bort fra spesifikke banenavn, da anlegget kun har én bane." else: bane_instruks = f'Finn banestatusen SPESIFIKT for banen som heter/omtales som: "{course_name}".' - # NYTT: Hvisker inn i øret til AI-en hvis vi har en instruks fra admin - ekstra_tekst = f"\nVIKTIG EKSTRA-INSTRUKS FRA ADMIN:\n{ai_instruction}\n" if ai_instruction else "" + ekstra_tekst = f"\n!!! VIKTIG EKSTRA-INSTRUKS FRA ADMIN (DENNE OVERSTYRER ALLE ANDRE REGLER) !!!:\n{ai_instruction}\n" if ai_instruction else "" - # 2. Selve promptet prompt = f""" Du er en ekspert på å lese norske golfklubbers nettsider for å finne banestatus. {bane_instruks} @@ -57,6 +53,15 @@ async def ask_llm_status(text, course_name, is_single_course, ai_instruction=Non {text[:15000]} """ + print("\n" + "="*60) + print(f"🤖 SENDER PROMPT TIL GEMINI FOR: '{course_name}'") + print(f"👉 STANDARD-INSTRUKS: {bane_instruks}") + if ai_instruction: + print(f"👉 ADMIN-HVISKER: {ai_instruction}") + clean_text_sample = " ".join(text.split())[:250] + print(f"👉 TEKST FRA NETTSIDEN (utdrag): '{clean_text_sample}...'") + print("="*60 + "\n") + try: response = await client.aio.models.generate_content( model='gemini-2.5-flash', @@ -64,15 +69,18 @@ async def ask_llm_status(text, course_name, is_single_course, ai_instruction=Non ) svar = response.text.strip().lower() - # 3. Sikkerhetsfilteret som matcher ordene i promptet + print(f" 🧠 GEMINI RÅ-SVAR: '{svar}'") + + # --- NYTT: SORTERT SIKKERHETSFILTER --- + # De lengste/mest spesifikke må stå først! gyldige_svar = [ - "aapen", - "stengt", "aapen_med_vintergreener", "aapner_snart", "stenger_snart", "under_utvikling", "nedlagt", + "stengt", + "aapen", "ukjent" ] @@ -146,7 +154,6 @@ async def run_daily_scraping(facility_ids=None): print(f"🚀 Starter sjekk {datetime.now().strftime('%H:%M:%S')}...") conn = await asyncpg.connect(DB_URL) - # --- NYTT: Filtrerer basert på valgte IDer fra Admin-panelet --- if facility_ids: print(f"📌 Kjører skraping KUN for anlegg-ID(er): {facility_ids}") facilities = await conn.fetch( @@ -163,7 +170,6 @@ async def run_daily_scraping(facility_ids=None): print("⚠️ Fant ingen anlegg å skrape.") await conn.close() return - # ---------------------------------------------------------------- changes, warnings, successes = [], [], [] @@ -174,7 +180,6 @@ async def run_daily_scraping(facility_ids=None): for f in facilities: method = f.get('scrape_method') or 'css_selector' - # THE KILL SWITCH - Hopper over manuelle baner if method == 'manual': successes.append(f"⏸️ {f['name']}: Hoppet over (Manuell overstyring)") print(f" ⏸️ Hopper over skraping av {f['name']} (Satt til Manuell)") @@ -187,7 +192,7 @@ async def run_daily_scraping(facility_ids=None): try: print(f"🔍 Besøker {f['name']} (Metode: {method})...") await page.goto(f['scrape_status_url'], timeout=60000, wait_until="domcontentloaded") - await asyncio.sleep(3) # Gir Javascript 3 sekunder på å bygge siden + await asyncio.sleep(3) full_text = "" @@ -228,30 +233,25 @@ async def run_daily_scraping(facility_ids=None): full_text = await element.inner_text() - # NY METODE: LLM PARSE (GEMINI) elif method == 'llm_parse': - # --- AUTO-KLIKKER --- - print(" 🖱️ Leter etter 'banestatus'-knapper å klikke på...") - knapper = await page.get_by_text(re.compile(r"banestatus", re.IGNORECASE)).all() + print(" 🖱️ Leter etter knapper å klikke på for å avdekke skjult tekst...") + knapper = await page.get_by_text(re.compile(r"banestatus|dagens status|se status|baneinfo", re.IGNORECASE)).all() for knapp in knapper: try: if await knapp.is_visible(): await knapp.click(timeout=3000) - print(" 🎯 Klikket på en banestatus-knapp! Venter 2 sekunder...") + print(" 🎯 Klikket på en status-knapp! Venter 2 sekunder...") await asyncio.sleep(2) break except Exception: pass - # -------------------- - # Kopierer all synlig tekst fra hele nettsiden element = page.locator("body").first if await element.count() == 0: warnings.append(f"❌ {f['name']}: Klarte ikke å lese siden for AI-tolkning") continue råtekst = await element.inner_text() - # Fjerner overflødige linjeskift for å komprimere teksten før sending til Gemini full_text = " ".join(råtekst.split()) else: @@ -262,15 +262,16 @@ async def run_daily_scraping(facility_ids=None): courses = await conn.fetch("SELECT id, name, status, scrape_keyword FROM courses WHERE facility_id = $1", f['id']) - # Sjekk om anlegget kun har én bane is_single_course = len(courses) == 1 for c in courses: - # HENTER STATUS VIA AI ELLER GAMMEL METODE if method == 'llm_parse': print(f" 🤖 Spør Gemini om status for '{c['name']}' (Singelbane: {is_single_course})...") new_status = await ask_llm_status(full_text, c['name'], is_single_course, f.get('ai_instruction')) + + print(" ⏳ Tar 5 sekunders pause for å spare Gemini-kvoten...") + await asyncio.sleep(5) else: new_status = interpret_status(full_text, c['scrape_keyword']) @@ -282,10 +283,10 @@ async def run_daily_scraping(facility_ids=None): if new_status != old_status and new_status != "ukjent": await conn.execute("UPDATE courses SET status = $1 WHERE id = $2", new_status, c['id']) changes.append(f"🔹 {f['name']} ({c['name']}): {old_status.upper()} ➔ {new_status.upper()}") - print(f"✅ Oppdatert status for {f['name']} - {c['name']}") + print(f" 🟢 KONKLUSJON: Status endret fra {old_status.upper()} til {new_status.upper()}") else: successes.append(f"✅ {f['name']} ({c['name']}): {new_status.upper()}") - print(f" - {c['name']}: Ingen endring ({new_status.upper()})") + print(f" ⚪ KONKLUSJON: Ingen endring. Banen er fortsatt {new_status.upper()}") except Exception as e: err_msg = str(e).split('\n')[0] @@ -300,7 +301,6 @@ async def run_daily_scraping(facility_ids=None): print("🏁 Ferdig.") if __name__ == "__main__": - # --- NYTT: Tar imot argumenter fra main.py (Background Task) --- parser = argparse.ArgumentParser(description="TeeOff Status Scraper") parser.add_argument("--ids", type=str, help="Kommaseparert liste med anleggs-IDer", default=None) args = parser.parse_args() diff --git a/frontend/src/app/FacilitySearch.tsx b/frontend/src/app/FacilitySearch.tsx index 1904530..de304e9 100644 --- a/frontend/src/app/FacilitySearch.tsx +++ b/frontend/src/app/FacilitySearch.tsx @@ -1,14 +1,16 @@ "use client"; /** - * TEE OFF SYSTEM INSTRUCTIONS - FACILITY CARDS v3.4 (STABLE) + * TEE OFF SYSTEM INSTRUCTIONS - FACILITY CARDS v3.6 (STABLE) * --------------------------------------------------------------------------- - * REGEL 1: Status-badge SKAL vises øverst til venstre. Bruk STATUS_MAP for tekst. + * REGEL 1: Status-badge SKAL vises øverst til venstre FOR ALLE BANER. + * Bruk STATUS_MAP for tekst. * REGEL 2: DATA-PARSING: Bruk parseJson() for 'course_statuses', 'amenities' og 'nsg_data' - * fordi API-et ofte returnerer disse som strenger. + * fordi API-et ofte returnerer disse som strenger. * REGEL 3: Avstand-pillen skal ha fargen #2d3319 (Mørk oliven) med hvit tekst. * REGEL 4: NSG (Blå 'N') og Golfamore (Oransje 'G') sirkler skal ha hvit kant (border-2). * REGEL 5: Bunnen: Antall Hull (grønn pill), Banetype (grå pill), og Ikon-sirkler. - * REGEL 6: LOSBY-LOGIKK: Sjekk alle baner i arrayen. Hvis én er åpen, vis 'aapen'. + * REGEL 6: Viser dato (f.eks "05. mars 2026") rett til høyre for øverste status-pille. + * Datoen har en svak bakgrunnsfarge for å sikre lesbarhet på lys bakgrunn. * --------------------------------------------------------------------------- */ @@ -75,7 +77,7 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie return (