Add hosted legal pages

This commit is contained in:
Meta Vens 2026-04-29 13:31:15 +01:00
commit 4de3e2406a
5 changed files with 284 additions and 0 deletions

View file

@ -923,6 +923,13 @@ def _landing_page_html(
)
def _static_html_page(static_dir: Path, filename: str) -> str:
styles_path = static_dir / "styles.css"
styles_version = int(styles_path.stat().st_mtime_ns) if styles_path.exists() else 0
template = (static_dir / filename).read_text(encoding="utf-8")
return template.replace("{{STYLES_VERSION}}", str(styles_version))
def create_app():
deps = _require_gui_dependencies()
FastAPI = deps["FastAPI"]
@ -1238,6 +1245,16 @@ def create_app():
)
)
return RedirectResponse(url="/app/")
@app.api_route("/privacy", methods=["GET", "HEAD"])
@app.api_route("/privacy/", methods=["GET", "HEAD"])
def privacy_policy():
return HTMLResponse(_static_html_page(static_dir, "privacy.html"))
@app.api_route("/terms", methods=["GET", "HEAD"])
@app.api_route("/terms/", methods=["GET", "HEAD"])
def terms_of_service():
return HTMLResponse(_static_html_page(static_dir, "terms.html"))
else: # pragma: no cover - only relevant before frontend assets exist
@app.get("/")
def root_missing():

View file

@ -229,6 +229,9 @@
<p class="copy-tag status-text">
Self-hosters can still go straight to <a href="/app/">the app</a>. Hosted mode just gets a front door instead of making everyone trip over raw controls immediately.
</p>
<p class="copy-tag status-text">
<a href="/privacy">Privacy Policy</a> · <a href="/terms">Terms of Service</a>
</p>
</section>
</main>
<script>

View file

@ -0,0 +1,113 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Privacy Policy - Repixelizer</title>
<link rel="stylesheet" href="/app/styles.css?v={{STYLES_VERSION}}">
<style>
.legal-page {
max-width: 920px;
}
.legal-page .panel-stack {
gap: 18px;
}
.legal-page h2 {
font-size: 1rem;
}
.legal-page ul {
display: grid;
gap: 8px;
margin: 0;
padding-left: 22px;
}
.legal-page li,
.legal-page p {
color: var(--ui-text-muted);
font-size: 1.05rem;
line-height: 1.35;
}
.legal-nav {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
</style>
</head>
<body>
<main class="shell app-shell legal-page">
<section class="app-shell-header ui-surface ui-frame">
<div class="brand-lockup">
<div class="brand-emblem" aria-hidden="true">
<img class="brand-emblem-art" src="/app/logos/repixelizer-logo-monogram.svg" alt="">
</div>
<div class="copy-stack">
<p class="eyebrow">Repixelizer</p>
<h1>Privacy Policy</h1>
<p class="copy-tag">Effective date: April 29, 2026</p>
</div>
</div>
<nav class="legal-nav" aria-label="Legal links">
<a class="button button-secondary" href="/">Home</a>
<a class="button button-secondary" href="/terms">Terms</a>
</nav>
</section>
<section class="card panel-stack ui-surface ui-frame">
<p>
Repixelizer is operated by GameCult. This policy describes the hosted demo at
<a href="https://repixelizer.gamecult.org">repixelizer.gamecult.org</a>.
</p>
<div class="copy-stack">
<h2>Information We Process</h2>
<ul>
<li>Images you upload for repixelizing, plus generated job outputs and progress data.</li>
<li>Basic account/authentication data returned by enabled login providers, such as provider user id, display name, username, and provider scopes.</li>
<li>Provider entitlement facts needed to decide access, such as Discord guild role matches.</li>
<li>Operational logs and request metadata needed to run, secure, debug, and abuse-protect the service.</li>
</ul>
</div>
<div class="copy-stack">
<h2>How We Use It</h2>
<ul>
<li>To run Repixelizer jobs and show you job status/results.</li>
<li>To authenticate access to the hosted demo through Heimdall, GameCult's shared auth service.</li>
<li>To enforce access rules, rate limits, queue limits, and security checks.</li>
<li>To diagnose failures and keep the service from falling over in a dramatic little heap.</li>
</ul>
</div>
<div class="copy-stack">
<h2>Sharing</h2>
<p>
We do not sell personal data. We share data only with service providers needed to operate the hosted demo,
with login providers you choose to authenticate through, or when required by law.
</p>
</div>
<div class="copy-stack">
<h2>Retention</h2>
<p>
Uploaded images, generated outputs, auth sessions, and logs are kept only as long as needed for operation,
debugging, security, or legal reasons. Hosted demo job data may be deleted at any time.
</p>
</div>
<div class="copy-stack">
<h2>Your Choices</h2>
<p>
You can sign out from the hosted demo at any time. For privacy questions or deletion requests, contact
<a href="mailto:meta@gamecult.org">meta@gamecult.org</a>.
</p>
</div>
</section>
</main>
</body>
</html>

View file

@ -0,0 +1,119 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Terms of Service - Repixelizer</title>
<link rel="stylesheet" href="/app/styles.css?v={{STYLES_VERSION}}">
<style>
.legal-page {
max-width: 920px;
}
.legal-page .panel-stack {
gap: 18px;
}
.legal-page h2 {
font-size: 1rem;
}
.legal-page ul {
display: grid;
gap: 8px;
margin: 0;
padding-left: 22px;
}
.legal-page li,
.legal-page p {
color: var(--ui-text-muted);
font-size: 1.05rem;
line-height: 1.35;
}
.legal-nav {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
</style>
</head>
<body>
<main class="shell app-shell legal-page">
<section class="app-shell-header ui-surface ui-frame">
<div class="brand-lockup">
<div class="brand-emblem" aria-hidden="true">
<img class="brand-emblem-art" src="/app/logos/repixelizer-logo-monogram.svg" alt="">
</div>
<div class="copy-stack">
<p class="eyebrow">Repixelizer</p>
<h1>Terms of Service</h1>
<p class="copy-tag">Effective date: April 29, 2026</p>
</div>
</div>
<nav class="legal-nav" aria-label="Legal links">
<a class="button button-secondary" href="/">Home</a>
<a class="button button-secondary" href="/privacy">Privacy</a>
</nav>
</section>
<section class="card panel-stack ui-surface ui-frame">
<p>
These terms apply to the hosted Repixelizer demo at
<a href="https://repixelizer.gamecult.org">repixelizer.gamecult.org</a>.
By using the hosted demo, you agree to these terms.
</p>
<div class="copy-stack">
<h2>Use Of The Service</h2>
<ul>
<li>Use Repixelizer only with images you have the right to process.</li>
<li>Do not upload unlawful, abusive, infringing, or privacy-violating material.</li>
<li>Do not attack, scrape, overload, bypass access controls, or interfere with the service.</li>
<li>The hosted demo is experimental and may change, rate-limit, or go offline without notice.</li>
</ul>
</div>
<div class="copy-stack">
<h2>Your Content</h2>
<p>
You keep whatever rights you already have in images you upload. You grant GameCult the limited permission
needed to process, store, transmit, and display your uploads and outputs so the hosted demo can function.
</p>
</div>
<div class="copy-stack">
<h2>Access</h2>
<p>
Hosted demo access may require authentication through supported providers such as Discord or Patreon.
Access can be limited, suspended, or revoked if needed to protect the service or enforce these terms.
</p>
</div>
<div class="copy-stack">
<h2>No Warranty</h2>
<p>
Repixelizer is provided as-is. Outputs may be wrong, ugly, cursed, or merely inconvenient. Use your own
judgment before relying on results in a real project.
</p>
</div>
<div class="copy-stack">
<h2>Liability</h2>
<p>
To the fullest extent allowed by law, GameCult is not liable for indirect, incidental, consequential, or
special damages arising from use of the hosted demo.
</p>
</div>
<div class="copy-stack">
<h2>Contact</h2>
<p>
For questions about these terms, contact <a href="mailto:meta@gamecult.org">meta@gamecult.org</a>.
</p>
</div>
</section>
</main>
</body>
</html>

View file

@ -414,9 +414,41 @@ def test_gui_hosted_root_serves_landing_page(monkeypatch, tmp_path: Path) -> Non
assert "Repixelizer hosted demo" in html
assert "Force fake pixel art back onto a real grid, then inspect each step with a full solver view." in html
assert 'href="/app/"' in html
assert 'href="/privacy"' in html
assert 'href="/terms"' in html
assert "Login with Discord" not in html
def test_gui_serves_public_legal_pages(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app
monkeypatch.setenv("REPIXELIZER_HOSTED_DEMO", "1")
monkeypatch.setenv("REPIXELIZER_SPOOL_DIR", str(tmp_path / "spool"))
app = create_app()
privacy_status, privacy_headers, privacy_body = asyncio.run(_get_response(app, "/privacy"))
terms_status, terms_headers, terms_body = asyncio.run(_get_response(app, "/terms"))
privacy_head_status, _privacy_head_headers, _privacy_head_body = asyncio.run(
_get_response(app, "/privacy", method="HEAD")
)
terms_head_status, _terms_head_headers, _terms_head_body = asyncio.run(
_get_response(app, "/terms", method="HEAD")
)
privacy_html = privacy_body.decode("utf-8")
terms_html = terms_body.decode("utf-8")
assert privacy_status == 200
assert "text/html" in privacy_headers["content-type"]
assert "Privacy Policy" in privacy_html
assert "meta@gamecult.org" in privacy_html
assert terms_status == 200
assert "text/html" in terms_headers["content-type"]
assert "Terms of Service" in terms_html
assert "meta@gamecult.org" in terms_html
assert privacy_head_status == 200
assert terms_head_status == 200
def test_gui_hosted_root_renders_heimdall_login_buttons(monkeypatch, tmp_path: Path) -> None:
from repixelizer.gui import create_app