diff --git a/backend/__pycache__/main.cpython-311.pyc b/backend/__pycache__/main.cpython-311.pyc index 841e055..c04ee5f 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 04665c8..05d027a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -36,10 +36,16 @@ ALGORITHM = "HS256" pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto") # --- PYDANTIC MODELLER --- +class CourseStatusUpdate(BaseModel): + id: int + status: str + class ScrapeSettingsUpdate(BaseModel): scrape_method: Optional[str] = None scrape_status_url: Optional[str] = None scrape_status_selector: Optional[str] = None + ai_instruction: Optional[str] = None + courses: Optional[List[CourseStatusUpdate]] = [] # NY MODELL FOR Å TA IMOT IDER FOR SCRAPING class ScrapeRunRequest(BaseModel): @@ -238,7 +244,7 @@ async def get_facilities(): rows = await conn.fetch(""" SELECT f.*, ( SELECT jsonb_agg(cs) FROM ( - SELECT name, status FROM courses + SELECT id, name, status FROM courses WHERE facility_id = f.id AND status != 'finnes_ingen_bane_to' ORDER BY is_main_course DESC, id ASC ) cs @@ -285,19 +291,26 @@ async def update_scrape_settings(facility_id: int, settings: ScrapeSettingsUpdat if not facility: raise HTTPException(status_code=404, detail="Anlegget finnes ikke.") - # Oppdater verdiene i databasen + # Oppdater verdiene i databasen inkludert AI instruks await conn.execute(""" UPDATE facilities SET scrape_method = $1, scrape_status_url = $2, - scrape_status_selector = $3 - WHERE id = $4 + scrape_status_selector = $3, + ai_instruction = $4 + WHERE id = $5 """, settings.scrape_method, settings.scrape_status_url, settings.scrape_status_selector, + settings.ai_instruction, facility_id) + # Hvis metoden er manuell, tvinger vi gjennom de nye banestatusene direkte + if settings.scrape_method == 'manual' and settings.courses: + for c in settings.courses: + await conn.execute("UPDATE courses SET status = $1 WHERE id = $2", c.status, c.id) + return {"status": "success", "message": f"Skrapeinnstillinger for anlegg ID {facility_id} ble oppdatert."} except Exception as e: diff --git a/backend/scrape_status.py b/backend/scrape_status.py index 68d9701..58ff8e9 100644 --- a/backend/scrape_status.py +++ b/backend/scrape_status.py @@ -26,7 +26,7 @@ DB_URL = os.getenv("DATABASE_URL", "postgresql://teeoff_admin:teeoff_secret_pass # 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): +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 @@ -35,10 +35,14 @@ async def ask_llm_status(text, course_name, is_single_course): 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 "" + # 2. Selve promptet prompt = f""" Du er en ekspert på å lese norske golfklubbers nettsider for å finne banestatus. {bane_instruks} + {ekstra_tekst} Svar KUN med nøyaktig ETT av disse ordene: - aapen (hvis banen er åpen/sommergreener) - stengt (hvis banen er lukket/stengt/frost/snø) @@ -146,13 +150,13 @@ async def run_daily_scraping(facility_ids=None): if facility_ids: print(f"📌 Kjører skraping KUN for anlegg-ID(er): {facility_ids}") facilities = await conn.fetch( - "SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method FROM facilities WHERE scrape_status_url IS NOT NULL AND id = ANY($1::int[])", + "SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method, ai_instruction FROM facilities WHERE scrape_status_url IS NOT NULL AND id = ANY($1::int[])", facility_ids ) else: print("🌍 Kjører skraping for ALLE anlegg med scrape_status_url...") facilities = await conn.fetch( - "SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method FROM facilities WHERE scrape_status_url IS NOT NULL" + "SELECT id, name, scrape_status_url, scrape_status_selector, scrape_method, ai_instruction FROM facilities WHERE scrape_status_url IS NOT NULL" ) if not facilities: @@ -168,17 +172,24 @@ async def run_daily_scraping(facility_ids=None): context = await browser.new_context() 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)") + continue + page = await context.new_page() try: await apply_stealth(page) except: pass try: - print(f"🔍 Besøker {f['name']} (Metode: {f.get('scrape_method') or 'css_selector'})...") + 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 full_text = "" - method = f.get('scrape_method') or 'css_selector' if method == 'css_selector': element = page.locator(f['scrape_status_selector']).first @@ -259,7 +270,7 @@ async def run_daily_scraping(facility_ids=None): # 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) + new_status = await ask_llm_status(full_text, c['name'], is_single_course, f.get('ai_instruction')) else: new_status = interpret_status(full_text, c['scrape_keyword']) diff --git a/frontend/src/app/admin/page.tsx b/frontend/src/app/admin/page.tsx index dd969a8..f1ab525 100644 --- a/frontend/src/app/admin/page.tsx +++ b/frontend/src/app/admin/page.tsx @@ -1,25 +1,38 @@ "use client"; /** - * TEE OFF ADMIN DASHBOARD v1.4 - LIVE PROGRESSION + * TEE OFF ADMIN DASHBOARD v1.7 - RESPONSIVT MED AI-HVISKER & KILL SWITCH * --------------------------------------------------------------------------- * PLASSERING: frontend/src/app/admin/page.tsx - * FUNKSJON: Starter bakgrunnsjobber og oppdaterer tabellen live. + * FUNKSJON: Live-oppdatering, massevalg, redigering og SAMMENTREKKBAR meny. * --------------------------------------------------------------------------- */ import { useState, useEffect } from 'react'; import { API_URL } from "@/config/constants"; -import ScrapeMethodSelect from "@/components/ScrapeMethodSelect"; +import ScrapeMethodSelect from "@/components/ScrapeMethodSelect"; export default function AdminDashboard() { const [facilities, setFacilities] = useState([]); const [loading, setLoading] = useState(true); const [selectedFacilities, setSelectedFacilities] = useState([]); - - // NYTT: Holder styr på om en skraping pågår akkurat nå const [isScraping, setIsScraping] = useState(false); - // Henter data fra databasen + // NY: State for å sjekke om sidebaren er klappet sammen + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + + const [editingFacility, setEditingFacility] = useState(null); + + // OPPDATERT: Lagt til ai_instruction og courses for manuell overstyring + const [editForm, setEditForm] = useState({ + scrape_status_url: '', + scrape_status_selector: '', + scrape_method: '', + ai_instruction: '', + courses: [] as any[] + }); + + const [isSaving, setIsSaving] = useState(false); + const fetchFacilities = () => { fetch(`${API_URL}/facilities`) .then(res => res.json()) @@ -30,12 +43,10 @@ export default function AdminDashboard() { .catch(() => setLoading(false)); }; - // Hent data ved første innlasting useEffect(() => { fetchFacilities(); }, []); - // NYTT: Hvis skraping pågår, oppdater tabellen hvert 10. sekund! useEffect(() => { let interval: NodeJS.Timeout; if (isScraping) { @@ -62,8 +73,13 @@ export default function AdminDashboard() { } }; - // NYTT: Sender IDene til API-et og starter auto-oppdatering - const handleRunScrapers = async () => { +const handleRunScrapers = async () => { + // Hvis den allerede skraper, lar vi brukeren trykke på knappen for å avbryte UI-oppdateringen + if (isScraping) { + setIsScraping(false); + return; + } + setIsScraping(true); try { const response = await fetch(`${API_URL}/admin/run-scraper`, { @@ -71,15 +87,13 @@ export default function AdminDashboard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ facility_ids: selectedFacilities }) }); - if (!response.ok) throw new Error("Kunne ikke starte skraping"); - // Valgfritt: Fjern avhukingene når jobben har startet - setSelectedFacilities([]); + // Beregn dynamisk timeout: 40 sekunder per valgte anlegg (Minimum 1 minutt) + const timeoutMs = Math.max(selectedFacilities.length * 40 * 1000, 60000); - // Stopper auto-oppdateringen etter 10 minutter (for sikkerhets skyld) - setTimeout(() => setIsScraping(false), 10 * 60 * 1000); - + setSelectedFacilities([]); + setTimeout(() => setIsScraping(false), timeoutMs); } catch (error) { console.error(error); alert("Feil ved start av skraperen."); @@ -87,49 +101,216 @@ export default function AdminDashboard() { } }; + // OPPDATERT: Laster inn eksisterende banestatuser og ai_instruction + const openEditModal = (facility: any) => { + setEditingFacility(facility); + setEditForm({ + scrape_status_url: facility.scrape_status_url || '', + scrape_status_selector: facility.scrape_status_selector || '', + scrape_method: facility.scrape_method || 'css_selector', + ai_instruction: facility.ai_instruction || '', + courses: facility.course_statuses ? facility.course_statuses.map((c: any) => ({id: c.id, name: c.name, status: c.status})) : [] + }); + }; + + const handleSaveEdit = async () => { + setIsSaving(true); + try { + const response = await fetch(`${API_URL}/admin/facilities/${editingFacility.id}/scrape-settings`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(editForm) + }); + if (!response.ok) throw new Error("Feil ved lagring"); + + setEditingFacility(null); + fetchFacilities(); + } catch (error) { + alert("Kunne ikke lagre endringene."); + console.error(error); + } finally { + setIsSaving(false); + } + }; + if (loading) return
LASTER DASHBORD...
; return ( -
-