From f25331c5f04cb7ad2be7b5b91e3fbdff0b490c02 Mon Sep 17 00:00:00 2001 From: Erol Date: Thu, 5 Mar 2026 15:11:04 +0100 Subject: [PATCH] Jobber med scraping --- backend/__pycache__/main.cpython-311.pyc | Bin 20371 -> 20350 bytes backend/main.py | 9 +-- backend/scrape_status.py | 50 ++++++++-------- frontend/src/app/FacilitySearch.tsx | 69 ++++++++++++++++------- 4 files changed, 76 insertions(+), 52 deletions(-) diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index c04ee5f4b6f7574067e9f434d3b64bcf518ff9af..8feb8a24a4b68916baea10381d6901b7e1c8bbe3 100644 GIT binary patch delta 362 zcmbO{pYh*3M!w~|yj%=GAmP0-)7)qy-xMLnhRKVC8kOcU)iRZEf}|iIixtMIVM<|1 zVOhgE*+W=XlC6eeHp5(|WsD3AtAQ8-7#UJHCf5n;GO|xzBP_|yQOjJ%oWc4l|~W;$(J*hGgL4~GL$oFa!u9}Il!e@P+5|ZpQoT(I{BlB#AX>$ZAM1E$sS_z zY%DyiE{r!O=ZZ-SurUZbFm6b{AY>0j9qdmh&lWqODCVfca8QTe$(H$`wUCoA^C4kg zAp4Mlkdp=TAq!R~+s!TFlb9HrHj7K`;$U31`I6E*4#tz44{D3DG5(o+-LQ=D?Pfot zzpPCE7&iM_Xs|O%ZZ5M=XJxeCe9vVE6Jx;U>FzUFRfHK`7#Fy#5MJW>fdRzELh5V| I_H|$a0Ou55-T(jq delta 429 zcmex2k8$#RM!w~|yj%=Gu$OOTW|8qmz9~YCO_LW3H9DuT%w?`+D&Yi4LO>QPj8(&w z!kWUmhHV)$1H)<{h5$x}5=I!Cfgy#xX0oA-C{qgO$s5H*_#zonIBS{f zm{T}|fvm~rgjJcjAX0LuQXoa;jO7d!%#jS`jGEk&HAD^=$rV(VWaQ^56ekxY7No`( zmn4>y7V8yMD(LEFrW7kw2`bnr=%=KX>F1T^ /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 (
-
@@ -84,27 +86,52 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
{processed.map((f: any) => { - // --- STATUS LOGIKK --- - const sArr = Array.isArray(f.statuses) ? f.statuses : []; - const activeStatus = sArr.find((s:any) => s.status === 'aapen') || sArr[0] || { status: 'ukjent' }; - const rawStatus = (activeStatus.status || "ukjent").toLowerCase(); - - let statusColor = "bg-gray-400"; - if (rawStatus === 'aapen') statusColor = "bg-[#8bc34a]"; - else if (rawStatus.includes('vinter') || rawStatus === 'stenger_snart') statusColor = "bg-[#ff5722]"; - else if (rawStatus === 'aapner_snart') statusColor = "bg-amber-500"; - else if (rawStatus === 'stengt') statusColor = "bg-red-600"; - else if (rawStatus === 'nedlagt') statusColor = "bg-black"; - else if (rawStatus === 'under_utvikling') statusColor = "bg-blue-500"; + // Sikrer at vi har en gyldig array for banestatuser + const sArr = Array.isArray(f.statuses) && f.statuses.length > 0 ? f.statuses : [{ status: 'ukjent', name: 'Hovedbane' }]; + + // Formater datoen pent: "05. mars 2026" + const lastUpdated = f.status_updated_at + ? new Date(f.status_updated_at).toLocaleDateString('nb-NO', { day: '2-digit', month: 'long', year: 'numeric' }) + : 'Ukjent'; return (
{f.name} - {/* Status Badge */} -
- {STATUS_MAP[rawStatus] || rawStatus} + {/* Status Badges for ALLE baner på anlegget */} +
+ {sArr.map((course: any, idx: number) => { + const rawStatus = (course.status || "ukjent").toLowerCase(); + + let statusColor = "bg-gray-400"; + if (rawStatus === 'aapen') statusColor = "bg-[#8bc34a]"; + else if (rawStatus.includes('vinter') || rawStatus === 'stenger_snart') statusColor = "bg-[#ff5722]"; + else if (rawStatus === 'aapner_snart') statusColor = "bg-amber-500"; + else if (rawStatus === 'stengt') statusColor = "bg-red-600"; + else if (rawStatus === 'nedlagt') statusColor = "bg-black"; + else if (rawStatus === 'under_utvikling') statusColor = "bg-blue-500"; + + return ( +
+
+ {sArr.length > 1 && ( + + {course.name} + + )} + {STATUS_MAP[rawStatus] || rawStatus} +
+ + {/* Dato-pille ved siden av den øverste status-pillen */} + {idx === 0 && ( +
+ {lastUpdated} +
+ )} +
+ ); + })}
{/* Avstandspille (Mørk oliven #2d3319) */} @@ -140,7 +167,7 @@ export default function FacilitySearch({ initialFacilities }: { initialFacilitie
G
)}
-
+
);