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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<p>Whichever way you choose a place, the coordinates are sent to the server only as part of
|
UI events v2: consented session tier, failed-search capture, privacy rewrite
Three operator decisions change v1's premises, so the design is now two tiers
with separate flags:
Tier A (THERMOGRAPH_EVENTS) is v1 unchanged -- anonymous hourly aggregates, no
identifier, nothing stored on the device, no consent needed, runs for everyone.
It stays the primary signal precisely so a poor consent rate cannot take the
numbers down with it, and so Tier B's rates can be calibrated against it to
measure the consent bias rather than ignoring it.
Tier B (THERMOGRAPH_EVENT_SESSIONS) adds an ephemeral per-tab id -- 16 random
bytes in sessionStorage, rotating on a 30-minute idle and a 2-hour absolute cap,
capped at 200 events, never linked to another session, device, or to the account
(api_event does not read the auth cookie and there is no user column). Rows land
in a 30-day hypertable with minute-granularity timestamps and an in-session
sequence number instead of precise clock times. Storing an identifier engages
ePrivacy Art. 5(3) and analytics is not strictly necessary, so it is gated on
opt-in consent: an equal-weight banner that mints nothing before "Allow",
one-click withdrawal in every footer that drops the live id immediately, and
GPC/DNT treated as a refusal already given.
THERMOGRAPH_SEARCH_MISS captures zero-result search text server-side in
api_suggest only -- normalised, rejected outright on any personal-data smell,
stored as a per-day count, and pruned below a three-person floor after a week.
The privacy page is rewritten rather than deferred: "no per-visitor identifier"
becomes false the moment Tier B ships. Tests assert the load-bearing promises so
the copy and the code cannot drift apart silently.
Raw-IP truncation is NOT implemented here (the logging pipeline owns it) but
UI-EVENTS.md states what the app side must do, and records that Caddy's default
JSON log already stores full request URIs -- so every search query is in Loki
with the client IP today, which the search-miss mitigations depend on fixing.
UI-EVENTS.md carries the v1-to-v2 diff, the consent reasoning including why
legitimate interest is not available, and an explicit argument that the session
identifier is the wrong trade at this traffic volume.
2026-07-23 23:11:06 +00:00
|
|
|
|
the ordinary request for that grid square's climate data. The place you picked is
|
|
|
|
|
|
remembered in your own browser's local storage, not on the server, so clearing your
|
|
|
|
|
|
browser data forgets it. <b>The coordinates are never recorded in the usage statistics
|
|
|
|
|
|
below</b> — those record only <i>how</i> you picked a place (map, search, or your
|
|
|
|
|
|
browser's location), never <i>where</i>.</p>
|
2026-07-21 17:48:55 +00:00
|
|
|
|
|
UI events v2: consented session tier, failed-search capture, privacy rewrite
Three operator decisions change v1's premises, so the design is now two tiers
with separate flags:
Tier A (THERMOGRAPH_EVENTS) is v1 unchanged -- anonymous hourly aggregates, no
identifier, nothing stored on the device, no consent needed, runs for everyone.
It stays the primary signal precisely so a poor consent rate cannot take the
numbers down with it, and so Tier B's rates can be calibrated against it to
measure the consent bias rather than ignoring it.
Tier B (THERMOGRAPH_EVENT_SESSIONS) adds an ephemeral per-tab id -- 16 random
bytes in sessionStorage, rotating on a 30-minute idle and a 2-hour absolute cap,
capped at 200 events, never linked to another session, device, or to the account
(api_event does not read the auth cookie and there is no user column). Rows land
in a 30-day hypertable with minute-granularity timestamps and an in-session
sequence number instead of precise clock times. Storing an identifier engages
ePrivacy Art. 5(3) and analytics is not strictly necessary, so it is gated on
opt-in consent: an equal-weight banner that mints nothing before "Allow",
one-click withdrawal in every footer that drops the live id immediately, and
GPC/DNT treated as a refusal already given.
THERMOGRAPH_SEARCH_MISS captures zero-result search text server-side in
api_suggest only -- normalised, rejected outright on any personal-data smell,
stored as a per-day count, and pruned below a three-person floor after a week.
The privacy page is rewritten rather than deferred: "no per-visitor identifier"
becomes false the moment Tier B ships. Tests assert the load-bearing promises so
the copy and the code cannot drift apart silently.
Raw-IP truncation is NOT implemented here (the logging pipeline owns it) but
UI-EVENTS.md states what the app side must do, and records that Caddy's default
JSON log already stores full request URIs -- so every search query is in Loki
with the client IP today, which the search-miss mitigations depend on fixing.
UI-EVENTS.md carries the v1-to-v2 diff, the consent reasoning including why
legitimate interest is not available, and an explicit argument that the session
identifier is the wrong trade at this traffic volume.
2026-07-23 23:11:06 +00:00
|
|
|
|
<h2>Usage statistics</h2>
|
|
|
|
|
|
<p>There is no third-party analytics service, no advertising tag, and nothing that follows
|
|
|
|
|
|
you to other sites. What the site does keep is in two parts, and only the second one
|
|
|
|
|
|
asks your permission.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3>Anonymous counts — always on</h3>
|
|
|
|
|
|
<p>The server keeps simple counts of things like "the calendar was opened 40 times this
|
|
|
|
|
|
hour" or "a search found nothing 12 times today", along with which site referred the
|
|
|
|
|
|
visit (the domain only, never the full link). No identifier of any kind is attached,
|
|
|
|
|
|
nothing is stored on your device, and there is no way to tell one visitor's counts from
|
|
|
|
|
|
another's — so there is nothing here that could be traced back to you. These counts are
|
|
|
|
|
|
kept for up to 400 days.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3>Session statistics — only if you say yes</h3>
|
|
|
|
|
|
<p>To answer questions like "do people who open the location picker actually get a
|
|
|
|
|
|
forecast, or give up half way?", the site has to see several actions as belonging to the
|
|
|
|
|
|
same visit. That means putting a random identifier in your browser, so
|
|
|
|
|
|
<b>we ask first, and it stays off unless you agree</b>.</p>
|
|
|
|
|
|
<p>If you agree:</p>
|
|
|
|
|
|
<ul>
|
|
|
|
|
|
<li>a random number is generated and kept in your browser's <b>session storage</b>, for
|
|
|
|
|
|
the current tab only. It is not a cookie, and it is not sent to any other site;</li>
|
|
|
|
|
|
<li>it changes after 30 minutes of inactivity, and after 2 hours regardless, so it cannot
|
|
|
|
|
|
follow you across a day, a device, or a return visit;</li>
|
|
|
|
|
|
<li>it disappears when you close the tab;</li>
|
|
|
|
|
|
<li>it records which views and controls you used, from a short fixed list — never what you
|
|
|
|
|
|
typed, never where you are, never which articles you read;</li>
|
|
|
|
|
|
<li>it is <b>never linked to your account</b>, even when you are signed in, and never
|
|
|
|
|
|
linked to your email address or your IP address;</li>
|
|
|
|
|
|
<li>the records are deleted after <b>30 days</b>.</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<p>You can change your mind at any time with the <b>Analytics</b> control at the bottom of
|
|
|
|
|
|
every page. Turning it off deletes the identifier from your browser immediately and stops
|
|
|
|
|
|
any further recording. Because the records are deliberately not linked to you, we cannot
|
|
|
|
|
|
go back and find your past ones to delete — which is part of why they are kept for such a
|
|
|
|
|
|
short time.</p>
|
|
|
|
|
|
<p>If your browser sends a <b>Global Privacy Control</b> or <b>Do Not Track</b> signal, the
|
|
|
|
|
|
site treats that as a no, does not ask, and records nothing extra.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h3>Searches that found nothing</h3>
|
|
|
|
|
|
<p>When you search for a place and the site finds no match, the text you typed may be kept
|
|
|
|
|
|
so that missing places can be added. It is stored on its own, with nothing else about you
|
|
|
|
|
|
attached, and only if it looks like a place name — anything containing an email address,
|
|
|
|
|
|
a web address or a long run of digits is discarded rather than stored. Anything only one
|
|
|
|
|
|
or two people ever searched for is deleted within a week; the rest is deleted after
|
|
|
|
|
|
90 days. Searches that <i>did</i> find a place are never stored as text.</p>
|
|
|
|
|
|
|
|
|
|
|
|
<h2>Server logs</h2>
|
|
|
|
|
|
<p>As on every website, the server records that a request happened — the page, the time and
|
|
|
|
|
|
the result. Network addresses are shortened before they are written down, so an individual
|
|
|
|
|
|
device cannot be picked out of the logs, and the logs are deleted after 30 days.</p>
|
2026-07-21 17:48:55 +00:00
|
|
|
|
|
|
|
|
|
|
<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
|
UI events v2: consented session tier, failed-search capture, privacy rewrite
Three operator decisions change v1's premises, so the design is now two tiers
with separate flags:
Tier A (THERMOGRAPH_EVENTS) is v1 unchanged -- anonymous hourly aggregates, no
identifier, nothing stored on the device, no consent needed, runs for everyone.
It stays the primary signal precisely so a poor consent rate cannot take the
numbers down with it, and so Tier B's rates can be calibrated against it to
measure the consent bias rather than ignoring it.
Tier B (THERMOGRAPH_EVENT_SESSIONS) adds an ephemeral per-tab id -- 16 random
bytes in sessionStorage, rotating on a 30-minute idle and a 2-hour absolute cap,
capped at 200 events, never linked to another session, device, or to the account
(api_event does not read the auth cookie and there is no user column). Rows land
in a 30-day hypertable with minute-granularity timestamps and an in-session
sequence number instead of precise clock times. Storing an identifier engages
ePrivacy Art. 5(3) and analytics is not strictly necessary, so it is gated on
opt-in consent: an equal-weight banner that mints nothing before "Allow",
one-click withdrawal in every footer that drops the live id immediately, and
GPC/DNT treated as a refusal already given.
THERMOGRAPH_SEARCH_MISS captures zero-result search text server-side in
api_suggest only -- normalised, rejected outright on any personal-data smell,
stored as a per-day count, and pruned below a three-person floor after a week.
The privacy page is rewritten rather than deferred: "no per-visitor identifier"
becomes false the moment Tier B ships. Tests assert the load-bearing promises so
the copy and the code cannot drift apart silently.
Raw-IP truncation is NOT implemented here (the logging pipeline owns it) but
UI-EVENTS.md states what the app side must do, and records that Caddy's default
JSON log already stores full request URIs -- so every search query is in Loki
with the client IP today, which the search-miss mitigations depend on fixing.
UI-EVENTS.md carries the v1-to-v2 diff, the consent reasoning including why
legitimate interest is not available, and an explicit argument that the session
identifier is the wrong trade at this traffic volume.
2026-07-23 23:11:06 +00:00
|
|
|
|
stores your email address and the places you are watching. None of the usage statistics
|
|
|
|
|
|
above are connected to it.</p>
|
2026-07-21 17:48:55 +00:00
|
|
|
|
|
|
|
|
|
|
<p class="muted">Questions: see <a href="{{ base }}/about">About & methodology</a>.</p>
|
UI events v2: consented session tier, failed-search capture, privacy rewrite
Three operator decisions change v1's premises, so the design is now two tiers
with separate flags:
Tier A (THERMOGRAPH_EVENTS) is v1 unchanged -- anonymous hourly aggregates, no
identifier, nothing stored on the device, no consent needed, runs for everyone.
It stays the primary signal precisely so a poor consent rate cannot take the
numbers down with it, and so Tier B's rates can be calibrated against it to
measure the consent bias rather than ignoring it.
Tier B (THERMOGRAPH_EVENT_SESSIONS) adds an ephemeral per-tab id -- 16 random
bytes in sessionStorage, rotating on a 30-minute idle and a 2-hour absolute cap,
capped at 200 events, never linked to another session, device, or to the account
(api_event does not read the auth cookie and there is no user column). Rows land
in a 30-day hypertable with minute-granularity timestamps and an in-session
sequence number instead of precise clock times. Storing an identifier engages
ePrivacy Art. 5(3) and analytics is not strictly necessary, so it is gated on
opt-in consent: an equal-weight banner that mints nothing before "Allow",
one-click withdrawal in every footer that drops the live id immediately, and
GPC/DNT treated as a refusal already given.
THERMOGRAPH_SEARCH_MISS captures zero-result search text server-side in
api_suggest only -- normalised, rejected outright on any personal-data smell,
stored as a per-day count, and pruned below a three-person floor after a week.
The privacy page is rewritten rather than deferred: "no per-visitor identifier"
becomes false the moment Tier B ships. Tests assert the load-bearing promises so
the copy and the code cannot drift apart silently.
Raw-IP truncation is NOT implemented here (the logging pipeline owns it) but
UI-EVENTS.md states what the app side must do, and records that Caddy's default
JSON log already stores full request URIs -- so every search query is in Loki
with the client IP today, which the search-miss mitigations depend on fixing.
UI-EVENTS.md carries the v1-to-v2 diff, the consent reasoning including why
legitimate interest is not available, and an explicit argument that the session
identifier is the wrong trade at this traffic volume.
2026-07-23 23:11:06 +00:00
|
|
|
|
<p class="muted consent-control" data-consent-control></p>
|
2026-07-21 17:48:55 +00:00
|
|
|
|
</article>
|
|
|
|
|
|
{% endblock %}
|