diff --git a/gen_pages.py b/gen_pages.py index b216bd7..313321a 100644 --- a/gen_pages.py +++ b/gen_pages.py @@ -41,10 +41,12 @@ Uruchomienie: python3 gen_pages.py from __future__ import annotations import html +import io import posixpath import re import shutil import sys +import zipfile from dataclasses import dataclass from pathlib import Path @@ -61,6 +63,16 @@ TERM_SHORT = "6–13.02" # nagłówek kolumny w tabeli porównawczej TERM_NOTE = "tydzień Fasching, szczyt sezonu" RESERVED = {"index.md", "log.md"} +# Pobieralna paczka źródeł dla AI (patrz sekcja niżej): same .md bundle'a +# plus wygenerowany AI-README.md, bez stron, zdjęć i generatorów. +ZIP_NAME = "ski-2027-kb.zip" +ZIP_ROOT = "ski-2027-kb" # katalog wewnątrz archiwum, żeby się nie rozsypało +AI_README = "AI-README.md" +# Stały czas modyfikacji wpisów: archiwum ma być bajt w bajt identyczne, +# dopóki nie zmieni się treść — inaczej każda regeneracja robiłaby gitowi +# diff z niczego. 1980-01-01 to minimum formatu ZIP. +ZIP_TIMESTAMP = (1980, 1, 1, 0, 0, 0) + # Publiczny adres wystawki — jedyne miejsce, w którym siedzi domena. Używa go # WYŁĄCZNIE pages/linki.html (spis do kopiowania i wysyłania grupie); wszystkie # pozostałe strony linkują względnie albo root-absolutnie, żeby działały tak @@ -1190,6 +1202,9 @@ TILES = [ ("/pages/linki.html", "Wszystkie linki", "Pełne adresy wszystkich stron wystawki, z przyciskiem „kopiuj” — " "do wrzucenia grupie."), + (f"/{PAGES_DIRNAME}/{ZIP_NAME}", "Pobierz bazę (.zip) — wrzuć do ChatGPT/Claude'a i pytaj", + "Wszystkie źródła wiedzy w markdownie, bez stron i zdjęć — model " + "przeczyta całość i odpowie na pytania o wyjazd."), ] @@ -1326,6 +1341,12 @@ def link_sections( ), Link(site_url("viz.html"), "Graf powiązań", "Struktura bazy — załącznik."), Link(site_url("pages/linki.html"), "Ten spis linków", "Strona, którą czytasz."), + Link( + site_url(f"{PAGES_DIRNAME}/{ZIP_NAME}"), + "Pobierz bazę (.zip) — wrzuć do ChatGPT/Claude'a i pytaj", + "Wszystkie źródła wiedzy w markdownie, bez stron i zdjęć — model " + "przeczyta całość i odpowie na pytania o wyjazd.", + ), ] used.add("wyjazd") @@ -1494,6 +1515,110 @@ def render_links(concepts: list[Concept], regions: list[Region]) -> str: return _page_shell("Linki", 0, content, script=LINKS_JS) +# --- pobieralna paczka źródeł dla AI ---------------------------------- + +def bundle_sources() -> list[Path]: + """Pliki .md bundle'a — z zachowaniem struktury katalogów.""" + return sorted( + path + for path in BUNDLE_ROOT.rglob("*.md") + if ".git" not in path.parts + and PAGES_DIRNAME not in path.relative_to(BUNDLE_ROOT).parts + ) + + +def bundle_date(concepts: list[Concept]) -> str: + """Data treści = najnowsze `ostatnia_aktualizacja` w bundle'u. + + Świadomie NIE bierzemy zegara: paczka ma być identyczna, dopóki nie + zmieni się wiedza. Data z zegara robiłaby nową wersję pliku każdego + dnia, także wtedy, gdy nic nie doszło. + """ + dates = [ + str(c.frontmatter.get("ostatnia_aktualizacja") or "").strip() for c in concepts + ] + return max((d for d in dates if d), default="—") + + +def render_ai_readme(concepts: list[Concept]) -> str: + """AI-README.md do paczki — skrót index.md dla modelu po drugiej stronie.""" + offers = [c for c in concepts if is_offer(c)] + active = [c for c in offers if not is_archived(c)] + regions = sorted( + c.id.split("/")[0] for c in concepts if c.id.endswith("/region") + ) + date = bundle_date(concepts) + return f"""# Ski 2027 — baza wiedzy wyjazdu ({TERM_LABEL}) + +## PL + +To baza wiedzy wyjazdu narciarskiego czterech rodzin (14 osób: 8 dorosłych, +4 nastolatków, 2 dzieci) z Warszawy w Alpy w terminie **{TERM_LABEL}** +({TERM_NOTE}). Format to OKF: markdown z frontmatterem YAML, gdzie jeden +plik = jeden koncept, `index.md` w każdym katalogu jest listingiem, a +`log.md` historią zmian; porównywane są {len(regions)} regiony +({", ".join(regions)}) w schemacie `/region.md` + +`/oferty/*.md`, razem {len(active)} aktualnych kart noclegów. +Odpowiadaj na pytania o ten wyjazd **wyłącznie na podstawie tych plików** i +podawaj, z którego pliku pochodzi odpowiedź. + +O pewności danych mówią dwa pola frontmattera: `status_ceny` +(ZWERYFIKOWANE = odczytane z cennika obiektu, SZACUNEK = nasza estymacja, +do potwierdzenia) oraz `{AVAILABILITY_FIELD}` ze statusem ✅ DOSTĘPNE +(zweryfikowane) / ❌ BRAK (zweryfikowane) / 🔍 BRAK DANYCH (do +weryfikacji). Nie przedstawiaj szacunków jako cen potwierdzonych i nie +zgaduj dostępności tam, gdzie stoi 🔍. Karty z `priorytet: ARCHIWALNA` +pochodzą z riserczu na poprzedni termin (16–23.01.2027) — traktuj je jako +historyczne, nie jako aktualną ofertę. + +**Data treści (najnowsza aktualizacja w bazie): {date}.** + +## EN + +This is the knowledge base of a ski trip for four families (14 people: +8 adults, 4 teenagers, 2 children) driving from Warsaw to the Alps on +**{TERM_LABEL}** (carnival week, peak season). The format is OKF: markdown +with YAML frontmatter, one file = one concept, `index.md` in each directory +is a listing and `log.md` is the change history; {len(regions)} regions +({", ".join(regions)}) are compared using the same schema +`/region.md` + `/oferty/*.md`, with {len(active)} current +accommodation cards in total. Answer questions about this trip **only from +these files** and say which file an answer comes from. + +Two frontmatter fields carry the confidence of the data: `status_ceny` +(ZWERYFIKOWANE = read from the property's own price list, SZACUNEK = our +estimate, unconfirmed) and `{AVAILABILITY_FIELD}` (✅ AVAILABLE, +verified / ❌ NOT AVAILABLE, verified / 🔍 NO DATA, to be checked). Never +present an estimate as a confirmed price and never guess availability where +the value is 🔍. Cards marked `priorytet: ARCHIWALNA` come from research +for the previous date (16–23.01.2027) — treat them as historical. + +**Content date (latest update in the base): {date}.** +""" + + +def write_bundle_zip(concepts: list[Concept]) -> Path: + """Buduje pages/ski-2027-kb.zip — deterministycznie, bajt w bajt.""" + entries: list[tuple[str, str]] = [ + (f"{ZIP_ROOT}/{AI_README}", render_ai_readme(concepts)) + ] + for path in bundle_sources(): + rel = path.relative_to(BUNDLE_ROOT).as_posix() + entries.append((f"{ZIP_ROOT}/{rel}", path.read_text(encoding="utf-8"))) + + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for name, text in sorted(entries): + info = zipfile.ZipInfo(name, date_time=ZIP_TIMESTAMP) + info.compress_type = zipfile.ZIP_DEFLATED + info.external_attr = 0o644 << 16 + archive.writestr(info, text.encode("utf-8")) + + target = PAGES_DIR / ZIP_NAME + target.write_bytes(buffer.getvalue()) + return target + + # --- raport braków ---------------------------------------------------- @@ -1566,6 +1691,8 @@ def main() -> int: path.write_text(markup, encoding="utf-8") written.append(path) + written.append(write_bundle_zip(concepts)) + print(f"Renderer markdown: {RENDERER}") print(f"Zapisano {len(written)} plików w {PAGES_DIR}:") for path in written: diff --git a/log.md b/log.md index d9df03e..2cb80ad 100644 --- a/log.md +++ b/log.md @@ -1,5 +1,13 @@ # Update Log +## 2026-08-04 + +* **Update**: **Zmiana terminu wyjazdu: 16–23.01 → 6–13.02.2027** i nałożenie riserczu 08.2026 na cały bundle. Nowy termin to tydzień **Fasching** (karnawał, Popielec 10.02) plus ferie bawarskie — szczyt sezonu, więc wszystkie budżety noclegowe poszły w górę, a [SFL](/sfl/region.md) traci styczniową przewagę cenową (u nich luty to pełny wysoki sezon). Zaktualizowane body i pola merytoryczne czterech regionów, dziesięciu kart noclegów i [szablonu maila](/szablon-maila.md). Każda karta noclegu ma nowe pole `dostepnosc_6_13_02_2027` z legendą ✅ DOSTĘPNE (zweryfikowane) / ❌ BRAK (zweryfikowane) / 🔍 BRAK DANYCH (do weryfikacji). Jedyny zweryfikowany status w bazie jest negatywny: [Glemm Lodge](/saalbach/oferty/glemm-lodge.md) ma na ten tydzień zajęte wszystkie 3 apartamenty (wolne 13–20.02 i 27.02–06.03) — twardy dowód presji na tydzień karnawału. [Wyjazd](/wyjazd.md) przepisany: nowa tabela porównawcza regionów, tabela dostępności i wnioski przekrojowe (Kronplatz najlepiej znosi termin), plus sekcja "Plan B na wypadek pełnego tygodnia". +* **Update**: Restrukturyzacja do symetrii — `region.md` i `oferty/` z korzenia przeniesione do [`saalbach/`](/saalbach/index.md), więc wszystkie cztery regiony siedzą w jednym schemacie `/region.md` + `/oferty/*.md`. Zdjęcia obiektów przetrwały bez ruszania: mapowanie oferta→zdjęcia jest kluczowane po polu `id`, nie po ścieżce. Dołożony [`saalbach/index.md`](/saalbach/index.md), indexy podkatalogów zaktualizowane, listing [index.md](/index.md) przebudowany na cztery równorzędne regiony. +* **Update**: Karty [dom dla grupy](/saalbach/oferty/dom-dla-grupy.md) i [AlpenParks Rehrenberg](/saalbach/oferty/alpenparks-rehrenberg.md) — spoza paczki riserczowej — oznaczone jako **ARCHIWALNE**: ich ceny i dostępność dotyczą starego terminu. Wypadają z tabeli porównawczej i ze statusów dostępności do osobnej sekcji "Archiwum" na dole porównania. +* **Update**: `gen_pages.py` — nowa kolumna "Dostępność 6–13.02" w tabeli porównawczej (emoji + werdykt w komórce, pełna notatka pod kursorem, sortowanie decyzyjne), wiersz z zweryfikowanym brakiem miejsc na czerwono, sekcja "Archiwum", termin wyjazdu w stałych `TERM_LABEL`/`TERM_SHORT`/`TERM_NOTE` (stąd biorą go nagłówki wszystkich stron i raport braków). Raport braków czyta dostępność z pola frontmattera zamiast zgadywać z nagłówków w body. +* **Update**: Nowa **pobieralna paczka źródeł dla AI**: `pages/ski-2027-kb.zip` (budowana przy każdej regeneracji, deterministycznie — bajt w bajt taka sama, dopóki nie zmieni się treść) z wszystkimi plikami `.md` bundle'a, `log.md` i wygenerowanym `AI-README.md` po polsku i angielsku (czym jest baza, jak czytać `status_ceny` i `dostepnosc_6_13_02_2027`, data treści). Link "Pobierz bazę (.zip) — wrzuć do ChatGPT/Claude'a i pytaj" na [starcie](/index.md) i w spisie linków. + ## 2026-08-03 * **Update**: Rozstrzygnięcie rozbieżności skipassowej w [Ischgl](/ischgl/region.md) (ischgl.com, cennik 26/27, sprawdzone 03.08.2026): 385/231 € to **VIP Skipass** — tylko dla nocujących w Ischgl/Mathon, 46 urządzeń, bez wyciągów Kappl; 451/257 € to **Silvretta Skipass** — dla gości spoza Ischgl/Mathon (w tym Kappl), 75 urządzeń. Przy PLANIE A (nocleg w Kappl) obowiązuje Silvretta, więc ceny w tabelach zostają. Poprawione dwa błędy w karcie: tabela 451/257 € była podpisana jako VIP, a sekcja "Teren" przypisywała VIP-owi zasięg całego Paznaun. Pułapka wiekowa 8–<17 potwierdzona, dopisany plus: dzieci <8 lat gratis — w karcie i w [wyjeździe](/wyjazd.md) (tabela porównawcza + wniosek nr 2). diff --git a/pages/linki.html b/pages/linki.html index c390cb0..b1b8a8c 100644 --- a/pages/linki.html +++ b/pages/linki.html @@ -158,9 +158,9 @@ td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius

Linki do wysłania

-

Wszystkie 24 stron wystawki jako pełne adresy — do skopiowania i wrzucenia grupie. Lista powstaje z tych samych danych co reszta stron, więc nowa oferta pojawi się tu sama.

+

Wszystkie 25 stron wystawki jako pełne adresy — do skopiowania i wrzucenia grupie. Lista powstaje z tych samych danych co reszta stron, więc nowa oferta pojawi się tu sama.

-

Wejście

+

Wejście

Regiony

Oferty — Saalbach

Oferty — Ischgl/Paznaun

diff --git a/pages/ski-2027-kb.zip b/pages/ski-2027-kb.zip new file mode 100644 index 0000000..af0871a Binary files /dev/null and b/pages/ski-2027-kb.zip differ diff --git a/pages/start.html b/pages/start.html index 421c382..70c8c86 100644 --- a/pages/start.html +++ b/pages/start.html @@ -159,7 +159,7 @@ td.thumb .placeholder { display: flex; width: 120px; height: 80px; border-radius

Ski 2027 — wyjazd 6–13.02.2027

6–13.02.2027 (tydzień Fasching, szczyt sezonu), 4 rodziny / 14 osób, wyjazd z Warszawy. Od czego zacząć:

- +

Regiony

Karta regionu = skipassy, dojazd, terminy, koszty życia i budżet całkowity.

diff --git a/viz.html b/viz.html index f4bd5a2..94fd681 100644 --- a/viz.html +++ b/viz.html @@ -226,8 +226,8 @@ hr { border: none; border-top: 1px solid #e2e8f0; margin: 14px 0; }