From 464781a7fd1ad3a74ca85751db5da1c36cd46c11 Mon Sep 17 00:00:00 2001 From: Martin Dvorak Date: Mon, 12 Oct 2026 21:26:55 +0000 Subject: [PATCH] Privacy policy, terms of service, cookie policy + a cookie notice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three plain-language documents in both app languages (lib/legal.ts, rendered by components/LegalPage.tsx): /privacy describes the local-first storage, the key never leaving the browser and the three opt-in server copies (sync, reminders, reports); /terms covers the GPL-3.0-only software, your own Ollama key, fair use and liability; /cookies states what is true — no cookies at all (no Set-Cookie, no document.cookie), strictly-necessary local storage instead, no tracking. A cookie notice tells every new browser the same thing once and remembers the dismissal (freezmo.consent.v1). It sits at z-44: below the immersive study feeds so it can never cover a session, above the phone tab bar. Settings links all three documents. The regression suite checks the pages render with real sections and that dismissing the notice sticks. Version 0.1.7, SW -> freezmo-v53. --- README.md | 16 ++ app/cookies/page.tsx | 20 +++ app/layout.tsx | 2 + app/privacy/page.tsx | 20 +++ app/terms/page.tsx | 20 +++ components/CookieConsent.tsx | 62 +++++++ components/LegalPage.tsx | 68 ++++++++ components/SettingsDialog.tsx | 32 ++++ lib/i18n-dict.ts | 18 +++ lib/legal.ts | 296 ++++++++++++++++++++++++++++++++++ package.json | 2 +- public/sw.js | 2 +- tests/regression.mjs | 27 ++++ 13 files changed, 583 insertions(+), 2 deletions(-) create mode 100644 app/cookies/page.tsx create mode 100644 app/privacy/page.tsx create mode 100644 app/terms/page.tsx create mode 100644 components/CookieConsent.tsx create mode 100644 components/LegalPage.tsx create mode 100644 lib/legal.ts diff --git a/README.md b/README.md index c6fa488..a35175e 100644 --- a/README.md +++ b/README.md @@ -1551,6 +1551,22 @@ Things I chose while building, with the reasoning: (one fixture per format, including a text PDF and a scanned PDF), 15/15 language preservation, 24/24 activity/scoring/feed checks and 11/11 real-model browser checks. +## Legal pages + +Freezmo ships its own plain-language documents, in both app languages, rendered by +`components/LegalPage.tsx` from `lib/legal.ts`: + +| Page | What it says | +| --- | --- | +| `/privacy` | Local-first storage, the key never leaving the browser, the three opt-in server copies (sync, reminders, reports), what the server does *not* store, and the third parties (Ollama, your browser's push service). | +| `/terms` | Free software under GPL-3.0-only with no warranty, your own Ollama key and its limits, fair use of the instance, your content, availability and liability. | +| `/cookies` | No cookies at all (verified: no `Set-Cookie`, no `document.cookie`), what the strictly-necessary local storage holds, no tracking, and how to clear it. | + +A cookie notice (`components/CookieConsent.tsx`) tells every new browser the same thing once and +remembers the dismissal (`freezmo.consent.v1`). It sits below the immersive feeds in the stacking +order, so it can never cover a study session, and above the phone tab bar. Settings links all three +documents. The documents describe what the code actually does — keep them in step with it. + ## License Freezmo is free software, licensed under the **GNU General Public License, version 3 only** diff --git a/app/cookies/page.tsx b/app/cookies/page.tsx new file mode 100644 index 0000000..788dee6 --- /dev/null +++ b/app/cookies/page.tsx @@ -0,0 +1,20 @@ +import { AppHeader } from "@/components/AppHeader"; +import { LegalPage } from "@/components/LegalPage"; +import { Sidebar, SidebarOffset } from "@/components/Sidebar"; + +export const metadata = { + title: "Cookie policy", +}; + +export default function CookiesPage() { + return ( +
+ + +
+ + +
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index e25fc1e..22ddbec 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata, Viewport } from "next"; import { IceBackdrop } from "@/components/IceBackdrop"; +import { CookieConsent } from "@/components/CookieConsent"; import { InstallSheet } from "@/components/InstallSheet"; import { OfflineBanner } from "@/components/OfflineBanner"; import { ServiceWorkerRegistrar } from "@/components/ServiceWorkerRegistrar"; @@ -104,6 +105,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {children} + diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx new file mode 100644 index 0000000..2d1ecab --- /dev/null +++ b/app/privacy/page.tsx @@ -0,0 +1,20 @@ +import { AppHeader } from "@/components/AppHeader"; +import { LegalPage } from "@/components/LegalPage"; +import { Sidebar, SidebarOffset } from "@/components/Sidebar"; + +export const metadata = { + title: "Privacy policy", +}; + +export default function PrivacyPage() { + return ( +
+ + +
+ + +
+
+ ); +} diff --git a/app/terms/page.tsx b/app/terms/page.tsx new file mode 100644 index 0000000..59c004c --- /dev/null +++ b/app/terms/page.tsx @@ -0,0 +1,20 @@ +import { AppHeader } from "@/components/AppHeader"; +import { LegalPage } from "@/components/LegalPage"; +import { Sidebar, SidebarOffset } from "@/components/Sidebar"; + +export const metadata = { + title: "Terms of service", +}; + +export default function TermsPage() { + return ( +
+ + +
+ + +
+
+ ); +} diff --git a/components/CookieConsent.tsx b/components/CookieConsent.tsx new file mode 100644 index 0000000..ac398bf --- /dev/null +++ b/components/CookieConsent.tsx @@ -0,0 +1,62 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; + +import { useI18n } from "@/lib/i18n"; + +const STORAGE_KEY = "freezmo.consent.v1"; + +/** + * The cookie notice. There are no cookies (see /cookies), so this is an + * honest "here is what we store in your browser" line, dismissed once and + * remembered. It sits below the immersive feeds in the stack (z-44) so it can + * never cover a study session, and above the phone tab bar. + */ +export function CookieConsent() { + const { t } = useI18n(); + const [visible, setVisible] = useState(false); + + useEffect(() => { + try { + setVisible(window.localStorage.getItem(STORAGE_KEY) === null); + } catch { + /* private mode: no storage, no banner */ + } + }, []); + + if (!visible) return null; + + const dismiss = () => { + try { + window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ at: Date.now() })); + } catch { + /* private mode */ + } + setVisible(false); + }; + + return ( +
+
+

+ {t("consent.body")}{" "} + + {t("consent.link")} + +

+ +
+
+ ); +} diff --git a/components/LegalPage.tsx b/components/LegalPage.tsx new file mode 100644 index 0000000..b5eae79 --- /dev/null +++ b/components/LegalPage.tsx @@ -0,0 +1,68 @@ +"use client"; + +import Link from "next/link"; + +import { useI18n } from "@/lib/i18n"; +import { LEGAL, type LegalSlug } from "@/lib/legal"; + +/** + * Renders one legal document in the current language (the text lives in + * lib/legal.ts, both languages side by side so they cannot drift apart). + */ +export function LegalPage({ slug }: { slug: LegalSlug }) { + const { locale, t } = useI18n(); + const document = LEGAL[slug][locale === "cs" ? "cs" : "en"]; + + return ( +
+
+

+ {t("legal.updated", { when: document.updated })} +

+

+ {document.title} +

+

{document.intro}

+ +
+ {document.sections.map((section) => ( +
+

{section.heading}

+ {section.paragraphs.map((paragraph) => ( +

+ {paragraph} +

+ ))} + {section.list ? ( +
    + {section.list.map((entry) => ( +
  • {entry}
  • + ))} +
+ ) : null} +
+ ))} +
+ +

{document.footer}

+ + +
+
+ ); +} diff --git a/components/SettingsDialog.tsx b/components/SettingsDialog.tsx index 8db0245..6a5ea11 100644 --- a/components/SettingsDialog.tsx +++ b/components/SettingsDialog.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { AnimatePresence, motion, useDragControls, useReducedMotion } from "framer-motion"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; @@ -529,6 +530,24 @@ export function SettingsDialog({ /> ) : null} + {/* Legal: the same three documents the cookie banner + links to, always reachable from Settings. */} +
+

+ {t("settings.legal")} +

+
+ + {t("legal.privacy")} + + + {t("legal.terms")} + + + {t("legal.cookies")} + +
+
{health?.mode === "direct-cloud" && health.needsKey ? (

{t("health.createKey.before")}{" "} @@ -808,6 +827,19 @@ export function SettingsDialog({

) : null} + {/* Legal: the same three documents the cookie banner links to. */} +
+ + {t("legal.privacy")} + + + {t("legal.terms")} + + + {t("legal.cookies")} + +
+ {/* keeps real content out of the 36px fade strip at the bottom */}
diff --git a/lib/i18n-dict.ts b/lib/i18n-dict.ts index bfb7ca8..b48dd81 100644 --- a/lib/i18n-dict.ts +++ b/lib/i18n-dict.ts @@ -997,6 +997,15 @@ const en: Record = { "chat.savedToNotes": "Saved to {deck}'s notes", "chat.savedToNotesTrimmed": "Saved to {deck}'s notes (trimmed to fit)", "chat.savedFromChat": "Saved from chat", + "legal.updated": "Last updated {when}", + "legal.privacy": "Privacy policy", + "legal.terms": "Terms of service", + "legal.cookies": "Cookie policy", + "settings.legal": "Legal", + "consent.title": "About cookies", + "consent.body": "Freezmo uses no cookies and no trackers — your decks, homework and settings stay in this browser.", + "consent.link": "Cookie policy", + "consent.ok": "Got it", "settings.version": "Version", "settings.chatNotes": "Keep notes text for chat", "settings.chatNotesNote": "Chat answers from the notes you added (stored with each deck). Turn off to use only the generated study content.", @@ -1940,6 +1949,15 @@ const cs: Record = { "chat.savedToNotes": "Uloženo do poznámek sady {deck}", "chat.savedToNotesTrimmed": "Uloženo do poznámek sady {deck} (zkráceno, aby se vešlo)", "chat.savedFromChat": "Uloženo z chatu", + "legal.updated": "Naposledy upraveno {when}", + "legal.privacy": "Zásady soukromí", + "legal.terms": "Podmínky použití", + "legal.cookies": "Zásady cookies", + "settings.legal": "Právní informace", + "consent.title": "O cookies", + "consent.body": "Freezmo nepoužívá cookies ani sledování — sady, úkoly a nastavení zůstávají v tomto prohlížeči.", + "consent.link": "Zásady cookies", + "consent.ok": "Rozumím", "settings.version": "Verze", "settings.chatNotes": "Uchovávat text poznámek pro chat", "settings.chatNotesNote": "Chat odpovídá z poznámek, které jsi přidal (jsou uložené u každé sady). Vypnutím bude používat jen vygenerovaný obsah.", diff --git a/lib/legal.ts b/lib/legal.ts new file mode 100644 index 0000000..4984051 --- /dev/null +++ b/lib/legal.ts @@ -0,0 +1,296 @@ +/** + * Privacy policy, terms of service and cookie policy — in both app languages. + * + * Written to describe what this app *actually* does (local-first storage, the + * key never leaving the browser, opt-in server copies), not a template. The + * pages that render this live in app/privacy, app/terms and app/cookies; the + * cookie banner links to the same text. Keep them in step with the code. + */ + +export type LegalSection = { + heading: string; + paragraphs: string[]; + list?: string[]; +}; + +export type LegalDocument = { + title: string; + updated: string; + intro: string; + sections: LegalSection[]; + footer: string; +}; + +const UPDATED = "October 2026"; + +const PRIVACY_EN: LegalDocument = { + title: "Privacy policy", + updated: UPDATED, + intro: + "Freezmo is a self-hosted study app with no accounts. This page describes exactly what happens to your data — nothing here is boilerplate.", + sections: [ + { + heading: "Everything lives in your browser", + paragraphs: [ + "Your decks, folders, homework, chats, study history and settings are stored in this browser (localStorage and IndexedDB). Nothing is uploaded unless you switch on one of the opt-ins below.", + "Clearing the site data deletes it. Settings → Export all data gives you a complete copy at any time.", + ], + }, + { + heading: "Your Ollama API key", + paragraphs: [ + "The key is kept in this browser only. It is sent to this Freezmo server for the single model call you asked for, forwarded to Ollama, and forgotten: never written to disk, never logged, never part of a backup, sync payload or share link.", + ], + }, + { + heading: "What the server stores — only if you opt in", + paragraphs: ["There is no other server-side data. Each of these is off until you turn it on:"], + list: [ + "Sync — an encrypted blob the server cannot read (the key stays in your browser). Revocable from Settings → Delete synced data.", + "Reminders — the push endpoint your browser gave us, its keys and a timestamp. No personal data, no history.", + "Reported questions — the question text, the deck and activity ids and a timestamp. These are how bad AI cards get fixed; they are the one thing an operator can read.", + ], + }, + { + heading: "What the server does not store", + paragraphs: [ + "No accounts, no email addresses, no analytics, no cookies, no tracking. The rate limiter keeps a client IP address in memory for a few minutes to stop floods and never writes it to disk.", + ], + }, + { + heading: "Third parties", + paragraphs: [ + "Model calls go to the Ollama endpoint this instance is configured for, using your key — Ollama's own privacy terms apply to them. Reminders travel through your browser's push service (Google, Mozilla or Apple). There is nothing else: no ads, no analytics, no embeds.", + ], + }, + { + heading: "Your choices", + paragraphs: [ + "Delete the local copy by clearing the site data (or Settings). Revoke the synced copy from Settings. Turn reminders off from Settings. If you want a reported question removed from the operator's file, ask the person running this instance — it is their file.", + ], + }, + ], + footer: "Plain-language description of how this app handles data — not legal advice.", +}; + +const PRIVACY_CS: LegalDocument = { + title: "Zásady ochrany soukromí", + updated: "říjen 2026", + intro: + "Freezmo je samoobslužná studijní aplikace bez účtů. Tahle stránka popisuje, co se přesně děje s tvými daty — nic z toho není šablona.", + sections: [ + { + heading: "Všechno zůstává v tvém prohlížeči", + paragraphs: [ + "Sady, složky, úkoly, chaty, historie učení i nastavení jsou uložené v tomto prohlížeči (localStorage a IndexedDB). Nic se neodesílá, dokud nezapneš některou z níže uvedených voleb.", + "Smazáním dat stránky je smažeš. Nastavení → Exportovat všechna data ti kdykoli dá úplnou kopii.", + ], + }, + { + heading: "Tvůj API klíč Ollama", + paragraphs: [ + "Klíč je uložený jen v tomto prohlížeči. Posílá se tomuto serveru Freezmo pro jeden požadovaný modelový dotaz, server ho předá Ollamě a zapomene ho: nikdy se neukládá na disk, neloguje se a není součástí zálohy, synchronizace ani sdíleného odkazu.", + ], + }, + { + heading: "Co server ukládá — jen když to zapneš", + paragraphs: ["Jiná data na serveru nejsou. Každá z těchhle věcí je vypnutá, dokud ji nezapneš:"], + list: [ + "Synchronizace — zašifrovaný blob, který server nedokáže přečíst (klíč zůstává v prohlížeči). Zrušitelný v Nastavení → Smazat synchronizovaná data.", + "Připomínky — push endpoint, který ti dal prohlížeč, jeho klíče a časové razítko. Žádná osobní data, žádná historie.", + "Nahlášené otázky — text otázky, ID sady a aktivity a časové razítko. Díky nim se opravují špatné AI kartičky; je to jediná věc, kterou provozovatel může číst.", + ], + }, + { + heading: "Co server neukládá", + paragraphs: [ + "Žádné účty, e-maily, analytiku, cookies ani sledování. Omezovač rychlosti drží IP adresu klienta pár minut v paměti, aby zastavil záplavy požadavků, a nikdy ji nezapisuje na disk.", + ], + }, + { + heading: "Třetí strany", + paragraphs: [ + "Modelové dotazy jdou na Ollama endpoint, který je tato instance nastavená používat, a to s tvým klíčem — platí na ně vlastní podmínky Ollamy. Připomínky putují přes push službu tvého prohlížeče (Google, Mozilla nebo Apple). Nic jiného tu není: žádné reklamy, analytika ani vložený obsah.", + ], + }, + { + heading: "Tvoje volby", + paragraphs: [ + "Lokální kopii smažeš smazáním dat stránky (nebo v Nastavení). Synchronizovanou kopii zrušíš v Nastavení. Připomínky vypneš v Nastavení. Pokud chceš, aby provozovatel odstranil nahlášenou otázku ze svého souboru, řekni mu to — ten soubor je jeho.", + ], + }, + ], + footer: "Popis v lidské řeči, jak aplikace nakládá s daty — není právní rada.", +}; + +const TERMS_EN: LegalDocument = { + title: "Terms of service", + updated: UPDATED, + intro: + "Freezmo is free software (GPL-3.0-only) that someone is hosting for you — usually a friend or yourself. These are the house rules for using that instance.", + sections: [ + { + heading: "The software comes as-is", + paragraphs: [ + "Freezmo is provided under the GNU General Public License v3 only, without warranty of any kind to the extent the law allows. AI output can be wrong: read it before you hand it in — that is what the note under every card is for.", + ], + }, + { + heading: "Your key, your usage", + paragraphs: [ + "You bring your own Ollama API key. Limits, costs and account standing are between you and Ollama. Never paste a key you do not own, and keep your own key to yourself.", + ], + }, + { + heading: "Fair use of the instance", + paragraphs: [ + "Don't flood it, don't try to break it and don't use it to attack anything else. The operator can rate-limit, block or shut the instance down at any time, and the app already refuses obvious abuse.", + ], + }, + { + heading: "Your content", + paragraphs: [ + "Your notes, decks, homework and chats are yours. They stay in your browser, so the operator cannot read them — the only exception is a question you report on purpose (see the privacy policy).", + ], + }, + { + heading: "Availability and changes", + paragraphs: [ + "This instance is a courtesy, with no uptime promise. The app and these terms may change; the current version always lives in the app, linked from Settings.", + ], + }, + { + heading: "Liability", + paragraphs: [ + "To the extent permitted by law, the operator and the authors are not liable for anything this app does or fails to do — including lost study time, lost data or a wrong answer in your homework.", + ], + }, + ], + footer: "Plain-language terms for a self-hosted app — not legal advice.", +}; + +const TERMS_CS: LegalDocument = { + title: "Podmínky použití", + updated: "říjen 2026", + intro: + "Freezmo je svobodný software (GPL-3.0-only), který ti někdo hostuje — většinou kamarád nebo ty sám. Tohle jsou pravidla pro používání téhle instance.", + sections: [ + { + heading: "Software je tak, jak je", + paragraphs: [ + "Freezmo se poskytuje pod licencí GNU General Public License v3 only, bez jakékoli záruky v rozsahu, který zákon dovoluje. AI výstup může být špatně: než ho odevzdáš, přečti si ho — od toho je poznámka pod každou kartičkou.", + ], + }, + { + heading: "Tvůj klíč, tvoje spotřeba", + paragraphs: [ + "Používáš vlastní API klíč Ollama. Limity, náklady a stav účtu jsou mezi tebou a Ollamou. Nikdy nevkládej klíč, který ti nepatří, a ten svůj nikomu nedávej.", + ], + }, + { + heading: "Férové používání instance", + paragraphs: [ + "Nezahlcuj ji, nezkoušej ji rozbít a nepoužívej ji k útoku na něco jiného. Provozovatel může kdykoli omezit rychlost, zablokovat přístup nebo instanci vypnout; aplikace už sama odmítá zjevné zneužití.", + ], + }, + { + heading: "Tvůj obsah", + paragraphs: [ + "Poznámky, sady, úkoly a chaty jsou tvoje. Zůstávají v tvém prohlížeči, takže je provozovatel nemůže číst — jediná výjimka je otázka, kterou sám nahlásíš (viz zásady soukromí).", + ], + }, + { + heading: "Dostupnost a změny", + paragraphs: [ + "Instance je zdvořilost, bez slibu dostupnosti. Aplikace i tyhle podmínky se mohou měnit; aktuální verze je vždy v aplikaci, odkaz v Nastavení.", + ], + }, + { + heading: "Odpovědnost", + paragraphs: [ + "V rozsahu, který zákon dovoluje, provozovatel ani autoři neodpovídají za nic, co aplikace udělá nebo neudělá — včetně ztraceného času na učení, ztracených dat nebo špatné odpovědi v úkolu.", + ], + }, + ], + footer: "Podmínky v lidské řeči pro samoobslužnou aplikaci — nejsou právní rada.", +}; + +const COOKIES_EN: LegalDocument = { + title: "Cookie policy", + updated: UPDATED, + intro: "Short version: Freezmo sets no cookies at all. This page exists because the law asks for one, and because it is worth being explicit.", + sections: [ + { + heading: "No cookies", + paragraphs: [ + "The server sends no Set-Cookie header and the app never touches document.cookie. There are no third-party embeds, no pixels and no tag managers.", + ], + }, + { + heading: "Local storage instead", + paragraphs: [ + "The app keeps your data in localStorage and IndexedDB — the same idea as a cookie, except it is never sent to the server automatically. It is strictly necessary for the app to work (it *is* the app's storage), which is why it does not need consent; the banner only tells you it happens.", + ], + list: [ + "Settings: language, theme, daily goal, haptics, chat notes preference.", + "Content: decks, folders, homework, chats, study history and schedule.", + "Bookkeeping: the sync id, the last-synced time, which questions you reported, whether you dismissed this banner.", + ], + }, + { + heading: "No tracking, no analytics, no ads", + paragraphs: ["Nothing measures you, and nothing is sold. The only counters the app keeps are the rate limits on the server, held in memory."], + }, + { + heading: "Clearing it", + paragraphs: [ + "Clear the site data in your browser to wipe the local copy, or use Settings → Export all data first if you want a backup. The opt-in server copies (sync, reminders, reports) are described in the privacy policy.", + ], + }, + ], + footer: "Plain-language description of what this app stores in your browser — not legal advice.", +}; + +const COOKIES_CS: LegalDocument = { + title: "Zásady používání cookies", + updated: "říjen 2026", + intro: "Krátce: Freezmo nepoužívá žádné cookies. Tahle stránka existuje proto, že to zákon vyžaduje — a protože je dobré to říct napřímo.", + sections: [ + { + heading: "Žádné cookies", + paragraphs: [ + "Server neposílá hlavičku Set-Cookie a aplikace se nikdy nedotkne document.cookie. Nejsou tu žádné vložené prvky třetích stran, pixely ani správci značek.", + ], + }, + { + heading: "Místo toho lokální úložiště", + paragraphs: [ + "Aplikace drží tvá data v localStorage a IndexedDB — stejná myšlenka jako cookie, jen se nikdy neposílají serveru automaticky. Je to nezbytné pro fungování aplikace (je to *její* úložiště), a proto to nepotřebuje souhlas; banner ti to jen oznamuje.", + ], + list: [ + "Nastavení: jazyk, téma, denní cíl, haptika, preference poznámek pro chat.", + "Obsah: sady, složky, úkoly, chaty, historie a plán učení.", + "Evidence: sync ID, čas poslední synchronizace, které otázky jsi nahlásil, jestli jsi zavřel tenhle banner.", + ], + }, + { + heading: "Žádné sledování, analytika ani reklamy", + paragraphs: ["Nic tě neměří a nic se neprodává. Jediné počítadla jsou limity rychlosti na serveru, držené v paměti."], + }, + { + heading: "Jak to smazat", + paragraphs: [ + "Smaž data stránky v prohlížeči, nebo si nejdřív udělej kopii přes Nastavení → Exportovat všechna data. Volitelné kopie na serveru (synchronizace, připomínky, hlášení) popisují zásady soukromí.", + ], + }, + ], + footer: "Popis v lidské řeči, co aplikace ukládá v prohlížeči — není právní rada.", +}; + +export type LegalSlug = "privacy" | "terms" | "cookies"; + +export const LEGAL: Record = { + privacy: { en: PRIVACY_EN, cs: PRIVACY_CS }, + terms: { en: TERMS_EN, cs: TERMS_CS }, + cookies: { en: COOKIES_EN, cs: COOKIES_CS }, +}; diff --git a/package.json b/package.json index 08976dd..6fac8a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freezmo", - "version": "0.1.6", + "version": "0.1.7", "private": true, "license": "GPL-3.0-only", "scripts": { diff --git a/public/sw.js b/public/sw.js index 241e2c4..b8b9872 100644 --- a/public/sw.js +++ b/public/sw.js @@ -9,7 +9,7 @@ * * Bumping CACHE_VERSION is how a new deploy invalidates old assets. */ -const CACHE_VERSION = "freezmo-v52"; +const CACHE_VERSION = "freezmo-v53"; const SHELL_CACHE = `${CACHE_VERSION}-shell`; const ASSET_CACHE = `${CACHE_VERSION}-assets`; diff --git a/tests/regression.mjs b/tests/regression.mjs index 6c6911f..3614466 100644 --- a/tests/regression.mjs +++ b/tests/regression.mjs @@ -111,5 +111,32 @@ const browser = await launchBrowser(); await context.close(); } +/* 5. legal pages + the cookie notice --------------------------------- */ +{ + const { context, page } = await makePage(browser); + await seed(page, { decks: [] }); + for (const [path, heading] of [ + ["/privacy", "Privacy policy"], + ["/terms", "Terms of service"], + ["/cookies", "Cookie policy"], + ]) { + await page.goto(`${BASE}${path}`, { waitUntil: "networkidle" }); + await page.waitForTimeout(600); + check(`legal: ${path} renders`, await page.getByRole("heading", { name: heading, level: 1 }).isVisible()); + } + check("legal: the pages carry real sections", (await page.locator("article section").count()) >= 3); + + await page.goto(BASE, { waitUntil: "networkidle" }); + await page.waitForTimeout(700); + const notice = page.getByRole("region", { name: "About cookies" }); + check("consent: the notice shows once", (await notice.count()) === 1); + await page.getByRole("button", { name: "Got it" }).click(); + await page.waitForTimeout(400); + await page.reload({ waitUntil: "networkidle" }); + await page.waitForTimeout(800); + check("consent: dismissing is remembered", (await notice.count()) === 0); + await context.close(); +} + await browser.close(); finish();