Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ primary law and reproducible methods that can support legal action when needed.
| River flow | Current BAFU discharge where a station passes the live-data rules; indicative estimates elsewhere |
| Relative to mean | Current discharge divided by a modelled annual mean; not a drought index |
| Temperature | Current BAFU station readings |
| Live legal screen | Fresh temperatures strictly above 25.0 °C, as review triggers under GSchV Annex 2 No. 12(4)—not findings of breach |
| Water quality | The current prepared NAWA TREND release, reduced to station-year summaries with exact laboratory rows on demand |
| Reservoirs | Federally supervised dam structures and a separate weekly regional energy-storage series |
| Ice | Glacier area in six inventories: 1850, 1931, 1973, 2010, 2016 and 2023 |
Expand All @@ -41,6 +42,11 @@ complete account of Swiss water. Important omissions are listed on the
- A discharge is **current** only when its station timestamp is valid, no more
than 30 minutes old, its value is finite and non-negative, and its unit is
verified. Stale or invalid observations remain inspectable but drive nothing.
- The live legal screen accepts the same 30-minute temperature window and flags
only reported values strictly above 25.0 °C. The ordinance concerns
human-caused temperature change after substantial mixing; the feed contains
no causation, reference-state, mixing, permit or exception evidence. An alert
starts review and never declares a violation.
- The station and river files contain unique identifiers. Where several valid
gauges share a model reach, the newest wins, then the closest snap, then the
station identifier.
Expand Down
2 changes: 1 addition & 1 deletion build/14-pages.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def load(lang):
# --- the mechanical half ---------------------------------------------------
ASSETS = ('fonts.css', 'tokens.css', 'style.css', 'site.css', 'favicon.svg', 'i18n.js',
'i18n-data.js', 'i18n-map.js', 'theme.js', 'fmt.js', 'gschg31.js',
'site.js', 'app.js')
'legal-alerts.js', 'site.js', 'app.js')
BASE = 'https://opengovclimate.ch/riverflow/'


Expand Down
17 changes: 16 additions & 1 deletion build/pages/index.en.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ <h1>Water and ice in Switzerland</h1>
<button type="button" id="dataViewOpen" aria-label="Explore this layer as data" title="Explore this layer as data">Data</button>
</nav>

<section id="liveAlerts" aria-labelledby="liveAlertsTitle">
<button type="button" id="liveAlertsToggle" aria-expanded="false" aria-controls="liveAlertsBody">
<span class="liveAlertHeading"><i aria-hidden="true"></i><span id="liveAlertsTitle">Legal screen</span></span>
<span id="liveAlertsSummary" role="status" aria-live="polite">Checking current readings…</span>
<span class="liveAlertChevron" aria-hidden="true"></span>
</button>
<div id="liveAlertsBody" hidden>
<p class="liveAlertScope">Fresh BAFU river-temperature readings are screened against the 25&nbsp;°C requirement in GSchV Annex&nbsp;2 No.&nbsp;12(4).</p>
<ol id="liveAlertsList"></ol>
<p class="liveAlertLimit">A reading above 25&nbsp;°C triggers review; it does not prove a breach. The rule concerns human-caused warming after substantial mixing. The live feed does not identify a heat input, reference state, mixing, permit or exception. Residual-flow and water-quality compliance need site-specific or non-live evidence.</p>
<p class="liveAlertLinks"><a href="https://www.fedlex.admin.ch/eli/cc/1998/2863_2863_2863/de" target="_blank" rel="noopener">Legal basis</a><a href="https://lindas.admin.ch/query" target="_blank" rel="noopener">Live data</a><a href="method.html#live">Method</a></p>
</div>
</section>

<nav id="modes" aria-label="Layer">
<div class="modeGroup" role="group" aria-label="Water now">
<span class="modeGroupLabel">Now</span>
Expand Down Expand Up @@ -442,6 +456,7 @@ <h2 id="introTitle">Follow the water. Protect its pathways.</h2>

<script src="fmt.js?v=20260822"></script>
<script src="gschg31.js?v=20260821"></script>
<script src="app.js?v=20260822c"></script>
<script src="legal-alerts.js?v=20260822"></script>
<script src="app.js?v=20260822d"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions build/pages/updates.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,40 @@
{
"<span class=\"liveAlertHeading\"><i aria-hidden=\"true\"></i><span id=\"liveAlertsTitle\">Legal screen</span></span> <span id=\"liveAlertsSummary\" role=\"status\" aria-live=\"polite\">Checking current readings…</span> <span class=\"liveAlertChevron\" aria-hidden=\"true\"></span>": {
"de": "<span class=\"liveAlertHeading\"><i aria-hidden=\"true\"></i><span id=\"liveAlertsTitle\">Rechtsprüfung</span></span> <span id=\"liveAlertsSummary\" role=\"status\" aria-live=\"polite\">Aktuelle Messwerte werden geprüft…</span> <span class=\"liveAlertChevron\" aria-hidden=\"true\"></span>",
"fr": "<span class=\"liveAlertHeading\"><i aria-hidden=\"true\"></i><span id=\"liveAlertsTitle\">Contrôle juridique</span></span> <span id=\"liveAlertsSummary\" role=\"status\" aria-live=\"polite\">Vérification des mesures actuelles…</span> <span class=\"liveAlertChevron\" aria-hidden=\"true\"></span>",
"it": "<span class=\"liveAlertHeading\"><i aria-hidden=\"true\"></i><span id=\"liveAlertsTitle\">Verifica giuridica</span></span> <span id=\"liveAlertsSummary\" role=\"status\" aria-live=\"polite\">Verifica delle misure attuali…</span> <span class=\"liveAlertChevron\" aria-hidden=\"true\"></span>",
"rm": "<span class=\"liveAlertHeading\"><i aria-hidden=\"true\"></i><span id=\"liveAlertsTitle\">Examinaziun giuridica</span></span> <span id=\"liveAlertsSummary\" role=\"status\" aria-live=\"polite\">Las mesiraziuns actualas vegnan examinadas…</span> <span class=\"liveAlertChevron\" aria-hidden=\"true\"></span>"
},
"Fresh BAFU river-temperature readings are screened against the 25&nbsp;°C requirement in GSchV Annex&nbsp;2 No.&nbsp;12(4).": {
"de": "Aktuelle Flusstemperaturen des BAFU werden an der Anforderung von 25&nbsp;°C nach Anhang&nbsp;2 Ziff.&nbsp;12 Abs.&nbsp;4 GSchV geprüft.",
"fr": "Les températures actuelles des cours d’eau de l’OFEV sont comparées à l’exigence de 25&nbsp;°C de l’annexe&nbsp;2 ch.&nbsp;12 al.&nbsp;4 OEaux.",
"it": "Le temperature attuali dei corsi d’acqua dell’UFAM sono confrontate con il requisito di 25&nbsp;°C dell’allegato&nbsp;2 n.&nbsp;12 cpv.&nbsp;4 OPAc.",
"rm": "Las temperaturas actualas dals flums da l’UFAM vegnan cumparegliadas cun la pretensiun da 25&nbsp;°C tenor l’agiunta&nbsp;2 cifra&nbsp;12 al.&nbsp;4 GSchV."
},
"A reading above 25&nbsp;°C triggers review; it does not prove a breach. The rule concerns human-caused warming after substantial mixing. The live feed does not identify a heat input, reference state, mixing, permit or exception. Residual-flow and water-quality compliance need site-specific or non-live evidence.": {
"de": "Eine Messung über 25&nbsp;°C löst eine Prüfung aus; sie beweist keine Rechtsverletzung. Die Regel betrifft eine menschengemachte Erwärmung nach weitgehender Durchmischung. Der Live-Datenstrom nennt weder Wärmeeintrag noch Referenzzustand, Durchmischung, Bewilligung oder Ausnahme. Die Einhaltung von Restwasser- und Qualitätsanforderungen braucht standortspezifische oder nicht aktuelle Belege.",
"fr": "Une mesure au-dessus de 25&nbsp;°C appelle un examen ; elle ne prouve pas une violation. La règle vise un réchauffement humain après large mélange. Le flux en direct n’identifie ni apport de chaleur, ni état de référence, ni mélange, ni autorisation, ni exception. Le respect des exigences de débit résiduel et de qualité requiert des preuves propres au site ou non disponibles en direct.",
"it": "Una misura sopra i 25&nbsp;°C richiede una verifica; non prova una violazione. La norma riguarda un riscaldamento antropico dopo ampia miscelazione. Il flusso in diretta non identifica apporto di calore, stato di riferimento, miscelazione, autorizzazione o eccezione. Il rispetto dei requisiti di deflusso residuale e qualità richiede prove specifiche del sito o non disponibili in diretta.",
"rm": "Ina mesiraziun sur 25&nbsp;°C pretenda in’examinaziun; ella na cumprova nagina violaziun. La regla pertutga in stgaudament chaschunà da l’uman suenter ina vasta maschaida. Las datas en direct n’identifitgeschan nagin apport da chalur, stadi da referenza, maschaida, permissiun u excepziun. Per examinar l’aua restanta e la qualitad dovri cumprovas specificas dal lieu u betg disponiblas en direct."
},
"<a href=\"https://www.fedlex.admin.ch/eli/cc/1998/2863_2863_2863/de\" target=\"_blank\" rel=\"noopener\">Legal basis</a><a href=\"https://lindas.admin.ch/query\" target=\"_blank\" rel=\"noopener\">Live data</a><a href=\"method.html#live\">Method</a>": {
"de": "<a href=\"https://www.fedlex.admin.ch/eli/cc/1998/2863_2863_2863/de\" target=\"_blank\" rel=\"noopener\">Rechtsgrundlage</a><a href=\"https://lindas.admin.ch/query\" target=\"_blank\" rel=\"noopener\">Livedaten</a><a href=\"method.html#live\">Methode</a>",
"fr": "<a href=\"https://www.fedlex.admin.ch/eli/cc/1998/2863_2863_2863/de\" target=\"_blank\" rel=\"noopener\">Base légale</a><a href=\"https://lindas.admin.ch/query\" target=\"_blank\" rel=\"noopener\">Données en direct</a><a href=\"method.html#live\">Méthode</a>",
"it": "<a href=\"https://www.fedlex.admin.ch/eli/cc/1998/2863_2863_2863/de\" target=\"_blank\" rel=\"noopener\">Base giuridica</a><a href=\"https://lindas.admin.ch/query\" target=\"_blank\" rel=\"noopener\">Dati in diretta</a><a href=\"method.html#live\">Metodo</a>",
"rm": "<a href=\"https://www.fedlex.admin.ch/eli/cc/1998/2863_2863_2863/de\" target=\"_blank\" rel=\"noopener\">Basa giuridica</a><a href=\"https://lindas.admin.ch/query\" target=\"_blank\" rel=\"noopener\">Datas en direct</a><a href=\"method.html#live\">Metoda</a>"
},
"<b>Live legal screen.</b> The page separately accepts a temperature only when its timestamp is valid and no more than 30 minutes old. A reported value strictly above 25.0&nbsp;°C triggers review against GSchV Annex&nbsp;2 No.&nbsp;12(4); exactly 25.0&nbsp;°C is at the ceiling, not above it. This is not a finding of breach. The feed does not establish human causation, the unaffected reference state, substantial mixing, the applicable permit or an exception.": {
"de": "<b>Live-Rechtsprüfung.</b> Die Seite übernimmt eine Temperatur gesondert nur mit gültigem Zeitstempel und einem Alter von höchstens 30 Minuten. Ein gemeldeter Wert strikt über 25,0&nbsp;°C löst eine Prüfung nach Anhang&nbsp;2 Ziff.&nbsp;12 Abs.&nbsp;4 GSchV aus; genau 25,0&nbsp;°C liegt an der Obergrenze, nicht darüber. Das ist keine Feststellung einer Rechtsverletzung. Der Datenstrom belegt weder menschliche Verursachung noch unbeeinflussten Referenzzustand, weitgehende Durchmischung, anwendbare Bewilligung oder Ausnahme.",
"fr": "<b>Contrôle juridique en direct.</b> La page n’accepte séparément une température que si son horodatage est valide et vieux de 30 minutes au plus. Une valeur signalée strictement supérieure à 25,0&nbsp;°C appelle un examen au regard de l’annexe&nbsp;2 ch.&nbsp;12 al.&nbsp;4 OEaux ; exactement 25,0&nbsp;°C se situe au plafond, non au-dessus. Ce n’est pas un constat de violation. Le flux n’établit ni cause humaine, ni état de référence non influencé, ni large mélange, ni autorisation applicable, ni exception.",
"it": "<b>Verifica giuridica in diretta.</b> La pagina accetta separatamente una temperatura solo se l’ora è valida e non più vecchia di 30 minuti. Un valore segnalato strettamente superiore a 25,0&nbsp;°C richiede una verifica rispetto all’allegato&nbsp;2 n.&nbsp;12 cpv.&nbsp;4 OPAc; esattamente 25,0&nbsp;°C è al massimo, non sopra. Non è un accertamento di violazione. Il flusso non dimostra causa antropica, stato di riferimento non influenzato, ampia miscelazione, autorizzazione applicabile o eccezione.",
"rm": "<b>Examinaziun giuridica en direct.</b> La pagina accepta separadamain ina temperatura mo sche ses temp è valid e maximalmain 30 minutas vegl. Ina valur annunziada strictamain sur 25,0&nbsp;°C pretenda in’examinaziun tenor l’agiunta&nbsp;2 cifra&nbsp;12 al.&nbsp;4 GSchV; exactamain 25,0&nbsp;°C è sin la limita, betg sur ella. Quai n’è betg ina constataziun d’ina violaziun. Las datas na cumprovan nagina chaschun umana, nagin stadi da referenza betg influenzà, nagina vasta maschaida, permissiun applitgabla u excepziun."
},
"<b>The live screen uses a strict comparison:</b> a fresh reported value above 25.0&nbsp;°C is a review trigger; 25.0&nbsp;°C itself is not labelled an exceedance. No alert is called a violation, because the live feed contains none of the attribution, reference-state, mixing, permit or exception evidence needed for that conclusion.": {
"de": "<b>Die Live-Prüfung vergleicht strikt:</b> Ein aktueller gemeldeter Wert über 25,0&nbsp;°C löst eine Prüfung aus; 25,0&nbsp;°C selbst wird nicht als Überschreitung bezeichnet. Kein Hinweis heisst Rechtsverletzung, denn der Live-Datenstrom enthält keine Belege zu Verursachung, Referenzzustand, Durchmischung, Bewilligung oder Ausnahme, die dieser Schluss voraussetzt.",
"fr": "<b>Le contrôle en direct applique une comparaison stricte :</b> une valeur actuelle signalée au-dessus de 25,0&nbsp;°C appelle un examen ; 25,0&nbsp;°C même n’est pas qualifié de dépassement. Aucune alerte n’est appelée violation, car le flux en direct ne contient aucune preuve d’attribution, d’état de référence, de mélange, d’autorisation ou d’exception nécessaire à cette conclusion.",
"it": "<b>La verifica in diretta applica un confronto rigoroso:</b> un valore attuale segnalato sopra i 25,0&nbsp;°C richiede una verifica; 25,0&nbsp;°C esatti non sono definiti un superamento. Nessun avviso è chiamato violazione, perché il flusso in diretta non contiene le prove di attribuzione, stato di riferimento, miscelazione, autorizzazione o eccezione necessarie a tale conclusione.",
"rm": "<b>L’examinaziun en direct cumpareglia strictamain:</b> ina valur actuala annunziada sur 25,0&nbsp;°C pretenda in’examinaziun; 25,0&nbsp;°C exactamain na vegn betg numnà ina surpassada. Nagin avis vegn numnà violaziun, perquai che las datas en direct na cuntegnan naginas cumprovas davart attribuziun, stadi da referenza, maschaida, permissiun u excepziun necessarias per quella conclusiun."
},
"1'041 published Q<sub>347</sub> points; illustrative, not a cantonal determination": {
"de": "1'041 publizierte Q<sub>347</sub>-Punkte; veranschaulichend, keine kantonale Festlegung",
"fr": "1 041 points Q<sub>347</sub> publiés ; illustration, non détermination cantonale",
Expand Down
39 changes: 38 additions & 1 deletion scripts/verify-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@ for (const file of landingFiles) {
!/<label\b[^>]*for=["']dataSearch["']/i.test(raw)) {
fail(file, 'active layers need a searchable semantic data-table alternative to the canvases');
}
if (!/<section\b[^>]*id=["']liveAlerts["'][\s\S]*?<button\b[^>]*id=["']liveAlertsToggle["'][\s\S]*?<ol\b[^>]*id=["']liveAlertsList["']/i.test(raw) ||
!/fedlex\.admin\.ch\/eli\/cc\/1998\/2863_2863_2863\/de/i.test(raw) ||
!/lindas\.admin\.ch\/query/i.test(raw) ||
!/method\.html#live/.test(raw)) {
fail(file, 'live legal screen is missing its accessible list, legal basis or method source');
}
}
const responsiveCssFile = join(site, 'style.css');
const responsiveCss = readFileSync(join(site, 'tokens.css'), 'utf8') + '\n' + readFileSync(responsiveCssFile, 'utf8');
Expand All @@ -176,6 +182,8 @@ for (const [label, pattern] of [
['useful smallest-phone evidence height', /@media \(max-width:\s*480px\) and \(max-height:\s*650px\) and \(orientation:\s*portrait\)\s*\{[\s\S]*?--workspace-h:\s*62dvh/s],
['sidebar project brief', /#workspace #intro\s*\{[^}]*position:\s*absolute;[^}]*inset:\s*0;[^}]*height:\s*100%/s],
['sidebar map readout', /#workspace #tooltip\s*\{[^}]*position:\s*static;[^}]*border-bottom:/s],
['sidebar legal screen', /#workspace #liveAlerts\s*\{[^}]*flex:\s*none;[^}]*border-bottom:/s],
['touch-sized legal screen', /#workspace #liveAlertsToggle\s*\{[^}]*min-height:\s*44px/s],
['high-contrast control boundary token', /--control-border:\s*#[0-9a-f]{6}/i],
]) if (!pattern.test(responsiveCss)) fail(responsiveCssFile, `missing responsive contract: ${label}`);
const appSource = readFileSync(join(site, 'app.js'), 'utf8');
Expand All @@ -193,7 +201,7 @@ if (!/workspace\.append\(el\)/.test(appSource) || !/dialog\.show\(\)/.test(appSo
fail(join(site, 'app.js'), 'project brief must occupy the evidence workspace without covering the map');
}
const workspaceComposition = /for \(const id of \[([^\]]+)\]\)/.exec(appSource.slice(0, 3500))?.[1] ?? '';
for (const id of ['titlebar', 'modes', 'mapControls', 'tooltip', 'legend', 'ribbon', 'panel', 'intro']) {
for (const id of ['titlebar', 'modes', 'mapControls', 'liveAlerts', 'tooltip', 'legend', 'ribbon', 'panel', 'intro']) {
if (!new RegExp(`['"]${id}['"]`).test(workspaceComposition)) {
fail(join(site, 'app.js'), `${id} is not composed into the evidence workspace`);
}
Expand Down Expand Up @@ -226,6 +234,35 @@ try {
fail(join(site, 'fmt.js'), `date-format validation failed: ${error.message}`);
}

// The only automatic legal screen is intentionally strict and freshness-bound:
// 25.0 C is the ceiling, not an exceedance; stale, future and invalid readings
// cannot become alerts. This executable contract guards the legal semantics from
// a visually plausible off-by-one or timestamp regression.
try {
const alertFile = join(site, 'legal-alerts.js');
const alertSandbox = {};
vm.createContext(alertSandbox);
vm.runInContext(readFileSync(alertFile, 'utf8'), alertSandbox);
const screen = alertSandbox.RiverflowLegalScreen;
const now = Date.parse('2026-08-22T12:00:00Z');
const result = screen.evaluateTemperature([
{ id: 'above', name: 'Above', temperature: 25.1, observedAt: '2026-08-22T11:50:00Z' },
{ id: 'at', name: 'At', temperature: 25.0, observedAt: '2026-08-22T11:51:00Z' },
{ id: 'below', name: 'Below', temperature: 24.9, observedAt: '2026-08-22T11:52:00Z' },
{ id: 'stale', name: 'Stale', temperature: 30, observedAt: '2026-08-22T11:29:59Z' },
{ id: 'future', name: 'Future', temperature: 30, observedAt: '2026-08-22T12:05:01Z' },
{ id: 'invalid', name: 'Invalid', temperature: 'not-a-number', observedAt: '2026-08-22T11:55:00Z' },
{ id: 'missing', name: 'Missing', temperature: null, observedAt: '2026-08-22T11:56:00Z' },
], { now });
if (screen.TEMPERATURE_LIMIT_C !== 25 || result.eligible.length !== 3 ||
result.above.length !== 1 || result.above[0].id !== 'above' ||
result.at.length !== 1 || result.at[0].id !== 'at') {
fail(alertFile, 'temperature screen is not strict, fresh-only and 25 C bound');
}
} catch (error) {
fail(join(site, 'legal-alerts.js'), `legal-screen validation failed: ${error.message}`);
}

// The sitemap must describe exactly the canonical publication surface.
const sitemapFile = join(site, 'sitemap.xml');
const sitemap = readFileSync(sitemapFile, 'utf8');
Expand Down
Loading