2026-07-21 17:48:55 +00:00
|
|
|
|
{% extends "base.html.j2" %}
|
|
|
|
|
|
{% block title %}{{ page_title }}{% endblock %}
|
|
|
|
|
|
{% block description %}{{ page_description }}{% endblock %}
|
|
|
|
|
|
{% block canonical_path %}{{ canonical_path }}{% endblock %}
|
|
|
|
|
|
{% block content %}
|
|
|
|
|
|
<nav class="breadcrumb" aria-label="Breadcrumb">
|
|
|
|
|
|
{% for label, href in breadcrumb %}{% if href %}<a href="{{ href }}">{{ label }}</a>{% else %}<span>{{ label }}</span>{% endif %}{% if not loop.last %} <span class="sep">›</span> {% endif %}{% endfor %}
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
<article class="climate-page">
|
|
|
|
|
|
<h1>Privacy</h1>
|
|
|
|
|
|
<p class="lede">Thermograph is free, has no ads, and needs no account. It is built so that
|
|
|
|
|
|
there is very little about you to collect in the first place.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Your location</h2>
|
Approximate-location fallback from the client IP (feature-flagged off)
A visitor who declines browser geolocation currently has no location at all —
the copy sends them to the map picker. This adds an opt-in fallback that
suggests a coarse city from the request's own IP, presented as a guess with a
one-tap correction, so the dead end has a way out.
backend/data/geoip.py does the lookup against a local MMDB file (DB-IP IP to
City Lite or GeoLite2 City — same format, either works, attribution derived
from the file's metadata). Off unless THERMOGRAPH_GEOIP is truthy AND the
database exists AND maxminddb imports; every degraded case — private/reserved/
CGNAT/IPv6-ULA addresses, unparseable input, no record, a country-centroid
record with no city, a record wider than the accuracy limit, a corrupt file —
returns None, which the route answers as 204 and the client treats exactly as
today.
The IP is read in memory and dropped. GET /api/v2/geoip runs no RunAudit,
records no metric dimension, and is excluded from the access log (its own
"geoip" traffic category) so no stored, joinable (IP, location) pair is ever
written. The response is no-store/Vary:* and takes no parameters.
Client-side rather than server-rendered on purpose: the homepage's HTML and
weak ETag must stay byte-identical for every visitor, or any cache added in
front of it can serve one visitor's city to another. The suggestion is fetched
only after a declined/unavailable prompt, and only when nothing is remembered.
A guess is never persisted — no localStorage write, no URL hash — and the hero
and results headings say "roughly near"/"near … approximate" rather than
letting the reverse-geocoded cell name a neighbourhood the lookup never knew.
The /privacy page's "never looks up your location from your IP address"
paragraph now follows the same flag out of the same env file, so the published
statement and the behaviour flip together.
Database lifecycle is a host-side systemd timer (infra/deploy/geoip-refresh.*)
that verifies a download opens and answers before swapping it in atomically,
bind-mounted read-only; geoip.py re-opens on mtime change, so a refresh needs
no restart. GEOIP-APPROX-LOCATION.md carries the database comparison, the
SSR-vs-client argument, the privacy analysis, and the open decisions.
2026-07-23 23:22:37 +00:00
|
|
|
|
{# This section states a fact about what the server actually does, so it follows
|
|
|
|
|
|
the THERMOGRAPH_GEOIP flag instead of being written once and left to rot.
|
|
|
|
|
|
See content.py's geoip_enabled(). #}
|
|
|
|
|
|
{% if geoip %}
|
|
|
|
|
|
<p>The precise way the site learns where you are is if you press <b>Use my location</b>,
|
|
|
|
|
|
which asks your browser for permission. You can decline, and picking a place on the map
|
|
|
|
|
|
works just as well.</p>
|
|
|
|
|
|
<p>If you decline, or your browser cannot offer it, we fall back to a rough guess at your
|
|
|
|
|
|
city from your IP address so there is something local to show instead of nothing. It is
|
|
|
|
|
|
always labelled as a guess, never as your position — it is accurate to tens of kilometres
|
|
|
|
|
|
at best, and one tap corrects it. It is also not remembered: a guess is never saved in
|
|
|
|
|
|
your browser or written into the page address, so only a place you actually pick is kept.</p>
|
|
|
|
|
|
<p>That lookup runs entirely on our own server, against a database file we host ourselves.
|
|
|
|
|
|
Your IP address is never sent to another company, and it is not stored: it is read from
|
|
|
|
|
|
the request, used to find a city, and discarded. It is deliberately kept out of our
|
|
|
|
|
|
request log for this feature too, so nothing anywhere links an address to a place.</p>
|
|
|
|
|
|
{% else %}
|
2026-07-21 17:48:55 +00:00
|
|
|
|
<p>Thermograph never looks up your location from your IP address. The only way the site
|
|
|
|
|
|
learns where you are is if you press <b>Use my location</b>, which asks your browser for
|
|
|
|
|
|
permission. You can decline, and picking a place on the map works just as well.</p>
|
Approximate-location fallback from the client IP (feature-flagged off)
A visitor who declines browser geolocation currently has no location at all —
the copy sends them to the map picker. This adds an opt-in fallback that
suggests a coarse city from the request's own IP, presented as a guess with a
one-tap correction, so the dead end has a way out.
backend/data/geoip.py does the lookup against a local MMDB file (DB-IP IP to
City Lite or GeoLite2 City — same format, either works, attribution derived
from the file's metadata). Off unless THERMOGRAPH_GEOIP is truthy AND the
database exists AND maxminddb imports; every degraded case — private/reserved/
CGNAT/IPv6-ULA addresses, unparseable input, no record, a country-centroid
record with no city, a record wider than the accuracy limit, a corrupt file —
returns None, which the route answers as 204 and the client treats exactly as
today.
The IP is read in memory and dropped. GET /api/v2/geoip runs no RunAudit,
records no metric dimension, and is excluded from the access log (its own
"geoip" traffic category) so no stored, joinable (IP, location) pair is ever
written. The response is no-store/Vary:* and takes no parameters.
Client-side rather than server-rendered on purpose: the homepage's HTML and
weak ETag must stay byte-identical for every visitor, or any cache added in
front of it can serve one visitor's city to another. The suggestion is fetched
only after a declined/unavailable prompt, and only when nothing is remembered.
A guess is never persisted — no localStorage write, no URL hash — and the hero
and results headings say "roughly near"/"near … approximate" rather than
letting the reverse-geocoded cell name a neighbourhood the lookup never knew.
The /privacy page's "never looks up your location from your IP address"
paragraph now follows the same flag out of the same env file, so the published
statement and the behaviour flip together.
Database lifecycle is a host-side systemd timer (infra/deploy/geoip-refresh.*)
that verifies a download opens and answers before swapping it in atomically,
bind-mounted read-only; geoip.py re-opens on mtime change, so a refresh needs
no restart. GEOIP-APPROX-LOCATION.md carries the database comparison, the
SSR-vs-client argument, the privacy analysis, and the open decisions.
2026-07-23 23:22:37 +00:00
|
|
|
|
{% endif %}
|
2026-07-21 17:48:55 +00:00
|
|
|
|
<p>Whichever way you choose a place, the coordinates are sent to the server only as part of
|
|
|
|
|
|
the ordinary request for that grid square's climate data, and are not logged beyond the
|
|
|
|
|
|
normal web-server request handling every site does. The place you picked is remembered in
|
|
|
|
|
|
your own browser's local storage, not on the server, so clearing your browser data
|
|
|
|
|
|
forgets it.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Analytics</h2>
|
|
|
|
|
|
<p>There is no analytics library, no cookies for tracking, and no per-visitor identifier.
|
|
|
|
|
|
The server keeps simple aggregate counts (how many people opened a page or tapped a
|
|
|
|
|
|
button, and which site referred them) with nothing tying any of it to an individual.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Email</h2>
|
|
|
|
|
|
<p>If you sign up for the monthly digest, your address is used for that digest and nothing
|
|
|
|
|
|
else. It is never sold or shared, and every message can unsubscribe you.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Accounts</h2>
|
|
|
|
|
|
<p>An account is optional and only exists to hold weather alerts you have asked for. It
|
|
|
|
|
|
stores your email address and the places you are watching.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<p class="muted">Questions: see <a href="{{ base }}/about">About & methodology</a>.</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
{% endblock %}
|