feat(dashboard): the console, phase 1 of the redesign (preview at /static/console.html)
The user chose direction B of the redesign: a full-screen operations console.
It is built next to the current dashboard, served by ServeDir at
/static/console.html (noindex, links back to /), and replaces dashboard.html
once it covers every feature (docs/DASHBOARD_REDESIGN.md, phase 4).
Phase 1:
- top bar: the Chiang Mai verdict (P.1 level + 24 h forecast, the current
dashboard's rule), gauges reporting, highest 24 h flood chance, data age
("overnight pause" instead of "stale" during RID's nightly gap, as in the
notifier), EN/TH, light/dark (dark default), Get alerts
- gauge list: one status rule for list, map and panel (normal, watch within
0.5 m or >= 25 % chance, warning, danger, no data), margin to the warning
level, 72 h sparkline, 6 h change; filter; sort by river order, closest to
the warning level, rising; arrow keys
- map: Ping mainstem and tributaries, status markers with code + level,
layers Rain 24 h (on by default only with heavy rain) and Flood zones
(auto-shown while P.1 is at a stage), one legend
- station panel: level, change, discharge (last RID value when the newest
hour is level-only), warning/danger levels with margins, capacity, level
scale with now and 24 h peak, 72 h chart with the forecast peaks, forecast
table with the model and issue time, the P.1 inundation stages, data links
- alerts drawer with the ntfy topics; selection in the URL (#P.84); refresh
every 5 minutes
- Inter and Noto Sans Thai self-hosted (src/static/fonts, 75 KB, OFL), no
emoji, Thai first, Bangkok time
tests/test_console.py: routed fetch paths, EN/TH parity, translated aria
labels, no emoji, fonts and licences, noindex. dev_proxy.py serves
src/static with real content types (it sent HTML as octet-stream).
This commit is contained in:
1 parent
0e30ddbcb6
commit
301a629b42
11 files changed
+1839
-3
No files matched your search
@@ -32,7 +32,10 @@ plus the monthly `water-monitor-retrain.timer` (Python, `.venv`, uv-managed). Up
|
|||||||
- Do not add Co-Authored-By trailers.
|
- Do not add Co-Authored-By trailers.
|
||||||
- The dashboard is a single file, `src/static/dashboard.html`, EN + TH via the `t()`
|
- The dashboard is a single file, `src/static/dashboard.html`, EN + TH via the `t()`
|
||||||
table: every user-visible string needs both languages. Every path it fetches must be
|
table: every user-visible string needs both languages. Every path it fetches must be
|
||||||
a route in `rust/collector/src/web/mod.rs` (tests/test_dashboard.py checks).
|
a route in `rust/collector/src/web/mod.rs` (tests/test_dashboard.py checks). Its
|
||||||
|
replacement, the console (`src/static/console.html`, preview at /static/console.html,
|
||||||
|
plan in docs/DASHBOARD_REDESIGN.md), follows the same rules plus no emoji
|
||||||
|
(tests/test_console.py); preview it with `uv run python scripts/dev_proxy.py`.
|
||||||
- Rust (`rust/`, see rust/README.md): `cargo fmt --all`, `cargo clippy --all-targets
|
- Rust (`rust/`, see rust/README.md): `cargo fmt --all`, `cargo clippy --all-targets
|
||||||
-- -D warnings`, `cargo test`; minimum version 1.88
|
-- -D warnings`, `cargo test`; minimum version 1.88
|
||||||
(`CARGO_TARGET_DIR=target/msrv cargo +1.88 check --all-targets --locked`). Read
|
(`CARGO_TARGET_DIR=target/msrv cargo +1.88 check --all-targets --locked`). Read
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ models/ trained bundles (gitignored) and evaluation results
|
|||||||
- [docs/MATRIX_QUICK_START.md](docs/MATRIX_QUICK_START.md) — the same alerts in a Matrix room
|
- [docs/MATRIX_QUICK_START.md](docs/MATRIX_QUICK_START.md) — the same alerts in a Matrix room
|
||||||
- [rust/README.md](rust/README.md) — the service: commands, configuration, develop, deploy
|
- [rust/README.md](rust/README.md) — the service: commands, configuration, develop, deploy
|
||||||
- [docs/RUST_REWRITE.md](docs/RUST_REWRITE.md) — why and how it moved from Python to Rust
|
- [docs/RUST_REWRITE.md](docs/RUST_REWRITE.md) — why and how it moved from Python to Rust
|
||||||
|
- [docs/DASHBOARD_REDESIGN.md](docs/DASHBOARD_REDESIGN.md) — the new console dashboard (preview at `/static/console.html`): layout, rules, phases
|
||||||
- [docs/references/NOTABLE_DOCUMENTS.md](docs/references/NOTABLE_DOCUMENTS.md) — official Thai government resources
|
- [docs/references/NOTABLE_DOCUMENTS.md](docs/references/NOTABLE_DOCUMENTS.md) — official Thai government resources
|
||||||
- Public overview: [buildfor.life/docs/tooling/ping-river-monitor](https://buildfor.life/docs/tooling/ping-river-monitor/)
|
- Public overview: [buildfor.life/docs/tooling/ping-river-monitor](https://buildfor.life/docs/tooling/ping-river-monitor/)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Dashboard redesign: the console
|
||||||
|
|
||||||
|
Status: **phase 1 done** (2026-10-19). The new dashboard is built next to the current
|
||||||
|
one and replaces it in phase 4.
|
||||||
|
|
||||||
|
- **Preview:** <https://water.buildfor.life/static/console.html>, after `git pull` on the
|
||||||
|
server (static files are read per request, no restart). It is not indexed
|
||||||
|
(`noindex`) and links back to the current dashboard.
|
||||||
|
- **Locally:** `uv run python scripts/dev_proxy.py 8765`, then
|
||||||
|
<http://localhost:8765/static/console.html>. The API is proxied to production.
|
||||||
|
- **Rules:** the same as for `dashboard.html`, plus no emoji (`tests/test_console.py`):
|
||||||
|
- every path it fetches is a Rust route;
|
||||||
|
- every string exists in EN and TH, and Thai is the default;
|
||||||
|
- timestamps are Bangkok wall-clock time.
|
||||||
|
|
||||||
|
## Why: the current page
|
||||||
|
|
||||||
|
Observed on 2026-10-19 at 1440 px and on a 390 px phone.
|
||||||
|
|
||||||
|
- **No clear first answer.** "Is Chiang Mai flooding?" is a thin green banner above
|
||||||
|
four KPI cards of equal weight. Two of them answer nothing for a resident: "Most
|
||||||
|
stressed gauge 16%" and "Chiang Mai river flow 53.9" (no unit in the number). At the
|
||||||
|
bottom there are five more (2,700,706 datapoints, 3,002 days, …).
|
||||||
|
- **The colours answer the wrong question.** Markers and river lines are coloured by
|
||||||
|
discharge class (m³/s), not by distance to the warning level, and the station list
|
||||||
|
shows discharge.
|
||||||
|
- **The map is noisy.** OSM terrain with Thai labels, numbered markers and animated
|
||||||
|
dashes. The legend covers about a third of the map, and more than half of the first
|
||||||
|
phone screen.
|
||||||
|
- **The header is busy.** It has six buttons with emoji (↻ 🌙 🔔 ▶); on a phone they
|
||||||
|
wrap into three rows.
|
||||||
|
- **Internal detail on the main page.** The model-skill table lists seven model hashes;
|
||||||
|
the history card is an empty 360 px box until a station is clicked.
|
||||||
|
- **Inconsistent styling.** A gradient background, several card and pill styles, three
|
||||||
|
greens.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Two directions were sketched on 2026-10-19 against live data (throwaway HTML, not kept):
|
||||||
|
|
||||||
|
- **A:** a public information page.
|
||||||
|
- **B:** an operations console.
|
||||||
|
|
||||||
|
The user chose **B**.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
One screen, no page scroll on a desktop:
|
||||||
|
|
||||||
|
| Area | Content |
|
||||||
|
|---|---|
|
||||||
|
| Top bar | brand · **Chiang Mai verdict** (P.1 level + 24 h forecast, always visible, click selects P.1) · gauges reporting · highest 24 h flood chance · data age · EN/ไทย · light/dark · Get alerts |
|
||||||
|
| Left | gauge list: status bar, code, name, margin to the warning level, 72 h sparkline, level and 6 h change; filter; sort by river order, closest to the warning level, rising; status legend |
|
||||||
|
| Centre | map: Ping mainstem and tributaries, gauges coloured by status with code + level labels, layers Rain 24 h and Flood zones, one legend |
|
||||||
|
| Right | selected gauge: level, 6 h change, discharge, warning/danger level with margins, capacity; level scale with now and 24 h peak; 72 h chart with the forecast peaks; forecast table (peak, chance of warning/danger at 6/12/24 h, model, issue time, disclaimer); for P.1 the seven inundation stages and the zones toggle; data links |
|
||||||
|
| Status bar | data credits, API, source code |
|
||||||
|
|
||||||
|
**Narrower screens:**
|
||||||
|
|
||||||
|
| Width | Layout |
|
||||||
|
|---|---|
|
||||||
|
| < 1280 px | The panel moves below list and map, in columns |
|
||||||
|
| < 860 px | One column: verdict, map, list, panel |
|
||||||
|
|
||||||
|
**Behaviour:**
|
||||||
|
- Selection is in the URL (`#P.84`).
|
||||||
|
- Arrow keys move through the list.
|
||||||
|
- Data refreshes every 5 minutes.
|
||||||
|
|
||||||
|
## Where every current element goes
|
||||||
|
|
||||||
|
| Current (`dashboard.html` id) | Console | Phase |
|
||||||
|
|---|---|---|
|
||||||
|
| `flood-verdict`, `verdict-*` | Verdict in the top bar | 1 |
|
||||||
|
| `station-count`, `last-updated`, `data-age`, `live-pill` | Top-bar KPIs (reporting, data age) | 1 |
|
||||||
|
| `river-flow`, `peak-station` (P.1 flow, most stressed gauge) | Discharge and capacity in the panel; "Closest to warning level" sort | 1 |
|
||||||
|
| `station-map`, `map-legend`, `legend-toggle`, `rain-toggle`, `zones-toggle` | Map, layer buttons, one legend | 1 |
|
||||||
|
| side list, `station-search` | Gauge list with filter and sorts | 1 |
|
||||||
|
| `forecast-card`, `p1-outlook`, `p1-peak`, `p1-stages`, `forecast-grid` | Panel: forecast table for every gauge, stages for P.1 | 1 |
|
||||||
|
| `alerts-panel`, `alerts-*` | Alerts drawer | 1 |
|
||||||
|
| `refresh-button` | Removed: refresh every 5 min, age in the top bar | 1 |
|
||||||
|
| `lang-toggle`, `theme-toggle` | EN / ไทย segmented control, icon button | 1 |
|
||||||
|
| `error`, `loading` | Map overlay with retry; "Loading" in the list | 1 |
|
||||||
|
| `thaiwater-sensors`, `sensors-*` | List tab + map layer | 2 |
|
||||||
|
| custom markers | Map layer | 2 |
|
||||||
|
| `history-card`, `history-*` | Panel history with ranges | 3 |
|
||||||
|
| `skill-panel`, `skill-*` | Panel section for P.1 | 3 |
|
||||||
|
| `db-stats`, `db-*` | Status-bar popover | 3 |
|
||||||
|
| `replay-2024`, `replay-*`, demo hooks | Replay/simulation mode with banner and mode pill | 4 |
|
||||||
|
|
||||||
|
## Design rules
|
||||||
|
|
||||||
|
- **Type:** Inter (Latin) and Noto Sans Thai, self-hosted in `src/static/fonts/` (75 KB, OFL). Numbers use tabular figures.
|
||||||
|
- **Colours:** CSS variables for light and dark, with dark the default; a saved choice wins (`ping-river-theme`, shared with the current dashboard).
|
||||||
|
- **Accent:** `#1d6fd6` light, `#4c9bf0` dark.
|
||||||
|
- **Status:** colours are used only for status, each with an `-ink` variant for text (WCAG AA on white).
|
||||||
|
- **One status rule** (`statusOf`) for the map, list and panel. It is display-only; the notifier keeps its own transitions.
|
||||||
|
|
||||||
|
| Status | Rule |
|
||||||
|
|---|---|
|
||||||
|
| Normal | none of the rules below |
|
||||||
|
| Watch | within 0.5 m of the warning level, or 24 h warning chance ≥ 25 % |
|
||||||
|
| Warning | level ≥ `threshold_warning` |
|
||||||
|
| Danger | level ≥ `threshold_danger` |
|
||||||
|
| No data | no reading for 24 h |
|
||||||
|
|
||||||
|
- **City verdict:** the current dashboard's rule — flooding from P.1 ≥ 3.70 m; "likely" at danger ≥ 50 % or warning ≥ 70 %; watch at warning ≥ 20 % or level ≥ 3.20 m.
|
||||||
|
- **Data age:** "feed stale" after 3 h, except during RID's overnight pause (21:00 to 09:00 with an evening reading). The pause shows as "overnight pause", the same rule as the notifier's `FEED_NIGHT_UNTIL_HOUR`.
|
||||||
|
- **Discharge:** comes with RID's report. When the newest hour is a level-only reading, the panel shows the last RID discharge with its time ("at 14:00").
|
||||||
|
- **Rain layer:** off by default unless any station has heavy rain (> 35 mm / 24 h); an explicit choice is remembered.
|
||||||
|
- **Flood zones:** shown automatically while P.1 is at or above a stage, unless hidden by the user.
|
||||||
|
|
||||||
|
## Phases
|
||||||
|
|
||||||
|
Each phase is one commit, checked with screenshots: 1440, 1100 and 390 px; light and dark; EN and TH.
|
||||||
|
|
||||||
|
1. **Done — Console shell with live data.**
|
||||||
|
- Layout, design tokens, fonts, EN/TH.
|
||||||
|
- Verdict and KPIs, gauge list with sparklines and sorts.
|
||||||
|
- Map with status markers, rain and flood-zone layers.
|
||||||
|
- Station panel (readings, scale, 72 h chart, forecast table, P.1 stages).
|
||||||
|
- Alerts drawer (ntfy topics); URL selection; 5-minute refresh.
|
||||||
|
- `tests/test_console.py`; `dev_proxy.py` serves `src/static` with proper content types.
|
||||||
|
2. **Map and basin.**
|
||||||
|
- **Basemap:** a self-hosted Protomaps extract of the Ping basin (PMTiles in `src/static/`, labels in Thai or English following the page language, light/dark themes). This replaces the dimmed OSM raster tiles, whose baked-in labels are the noisiest part of the screen.
|
||||||
|
- **Server:** check that ServeDir answers HTTP range requests (PMTiles needs them); add range support to `dev_proxy.py`.
|
||||||
|
- **ThaiWater/HII stations:**
|
||||||
|
- A tab in the list.
|
||||||
|
- A map layer: % of bank height, `/api/hii/waterlevel/latest`, falling back to `/sensors/thaiwater`.
|
||||||
|
- **Custom markers:** `custom-markers.json`, with their flood-zone popup.
|
||||||
|
3. **Station depth.**
|
||||||
|
- **History in the panel:** range 24 h / 7 d / 30 d / 90 d / all / custom dates, level and discharge, issued 24 h forecasts overlaid, flood bands; Chart.js or SVG.
|
||||||
|
- **P.1 forecast skill** ("Is the model getting better?") as a panel section, from `/api/forecast/skill`.
|
||||||
|
- **Data statistics:** a status-bar popover, from `/api/stats`.
|
||||||
|
4. **Modes and the swap.**
|
||||||
|
- **October 2024 replay:** banner, clock, markers and verdict follow the replay; zones auto-show.
|
||||||
|
- **Demo hooks:** `?demo_level=`, `?demo_rise=`. A mode pill (LIVE / REPLAY / SIMULATION) never relabels a replay or simulation as live.
|
||||||
|
- **The swap:** `console.html` becomes `dashboard.html`; the old file and its tests go. `tests/test_console.py` absorbs the remaining checks of `tests/test_dashboard.py` (one P.1 level, SEO tags; the robots `noindex` is removed).
|
||||||
|
- **Docs:** README screenshot and the CLAUDE.md rule.
|
||||||
|
5. **Polish.**
|
||||||
|
- Phone layout pass.
|
||||||
|
- Native-speaker review of the Thai strings.
|
||||||
|
- Keyboard and screen reader pass.
|
||||||
|
- Lighthouse ≥ 90 (performance, accessibility).
|
||||||
|
- No console errors.
|
||||||
|
|
||||||
|
## Data notes from building phase 1
|
||||||
|
|
||||||
|
- **Negative levels.** P.4A (−1.28 m) and P.67 (−0.59 m) report negative levels: gauge datums, not errors. The panel's scale starts at the lowest of 0 and the station's 72 h readings.
|
||||||
|
- **P.1 discharge disagrees between endpoints.** At 15:00 on 2026-10-19, `/measurements/latest` had level 1.30 m with no discharge, while `/measurements/history` had 26.0 m³/s with no capacity. RID's 14:00 report said 53.9 m³/s. The console shows the last RID discharge. Worth checking where the 26.0 comes from.
|
||||||
|
- **P.84 spike.** A one-hour spike at 13:00 the same day: 0.73 → 1.05 → 0.78 m.
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
"""Serve the working-copy dashboard locally with API calls proxied to the
|
"""Serve the working-copy dashboard locally with API calls proxied to the
|
||||||
live server, so browser-side changes can be checked against real data
|
live server, so browser-side changes can be checked against real data
|
||||||
before deploy. Usage: python scripts/dev_proxy.py [port]"""
|
before deploy. Usage: python scripts/dev_proxy.py [port]
|
||||||
|
|
||||||
|
/ is src/static/dashboard.html, /static/... any file in src/static (e.g. the
|
||||||
|
console preview at /static/console.html), everything else the live server."""
|
||||||
|
|
||||||
import http.server
|
import http.server
|
||||||
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
@@ -30,8 +34,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
ctype = (
|
ctype = (
|
||||||
"application/json"
|
"application/json"
|
||||||
if f.suffix in (".json", ".geojson")
|
if f.suffix in (".json", ".geojson")
|
||||||
else "application/octet-stream"
|
else mimetypes.guess_type(f.name)[0] or "application/octet-stream"
|
||||||
)
|
)
|
||||||
|
if ctype.startswith("text/"):
|
||||||
|
ctype += "; charset=utf-8"
|
||||||
self._send(200, ctype, f.read_bytes())
|
self._send(200, ctype, f.read_bytes())
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,13 @@
|
|||||||
|
# Fonts
|
||||||
|
|
||||||
|
Self-hosted so the dashboard makes no request to Google. Both fonts are under the
|
||||||
|
SIL Open Font License 1.1 (`*-OFL.txt` next to them).
|
||||||
|
|
||||||
|
| File | Font | Subset |
|
||||||
|
|---|---|---|
|
||||||
|
| `inter-latin.woff2` | Inter (variable, weight 400–700), <https://github.com/rsms/inter> | Latin |
|
||||||
|
| `noto-sans-thai-thai.woff2` | Noto Sans Thai (variable, weight 400–700), <https://github.com/notofonts/thai> | Thai |
|
||||||
|
|
||||||
|
Both files are Google Fonts' woff2 subsets (`fonts.googleapis.com/css2?family=Inter:wght@400..700`
|
||||||
|
and `family=Noto+Sans+Thai:wght@400..700`, fetched with a woff2-capable user agent).
|
||||||
|
The `unicode-range` of each `@font-face` in `console.html` matches its subset.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/thai)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -0,0 +1,100 @@
|
|||||||
|
"""The console dashboard (src/static/console.html, docs/DASHBOARD_REDESIGN.md).
|
||||||
|
|
||||||
|
Served at /static/console.html while it is built; it replaces dashboard.html
|
||||||
|
once it covers every feature. The rules are the dashboard's: every fetched
|
||||||
|
path is routed, every string has EN and TH, no emoji in the interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATIC = Path(__file__).parents[1] / "src" / "static"
|
||||||
|
CONSOLE = STATIC / "console.html"
|
||||||
|
ROUTER = Path(__file__).parents[1] / "rust" / "collector" / "src" / "web" / "mod.rs"
|
||||||
|
|
||||||
|
|
||||||
|
def _html() -> str:
|
||||||
|
return CONSOLE.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _body(html: str) -> str:
|
||||||
|
return html[html.index("<body>") : html.index("<script src=")]
|
||||||
|
|
||||||
|
|
||||||
|
def _tables(html: str) -> dict:
|
||||||
|
start = html.index("const STRINGS = {")
|
||||||
|
end = html.index("// Thai unless the visitor's browser", start)
|
||||||
|
block = html[start:end]
|
||||||
|
tables = {}
|
||||||
|
for lang in ("en", "th"):
|
||||||
|
section = re.search(rf"\n {lang}: {{\n(.*?)\n }},\n", block, re.S)
|
||||||
|
assert section, f"{lang} table not found in STRINGS"
|
||||||
|
tables[lang] = set(re.findall(r"^\s{12}'([^']+)':", section.group(1), re.M))
|
||||||
|
return tables
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_fetched_path_is_routed():
|
||||||
|
routes = set(re.findall(r'\.route\(\s*"([^"]+)"', ROUTER.read_text("utf-8")))
|
||||||
|
patterns = [
|
||||||
|
re.compile("^" + re.sub(r"\\\{[^}]+\\\}", "[^/]+", re.escape(r)) + "$")
|
||||||
|
for r in routes
|
||||||
|
]
|
||||||
|
fetched = set(re.findall(r"fetch\(\s*[`'\"](/[^`'\"?$]*)", _html()))
|
||||||
|
assert {"/stations", "/forecast", "/api/notifications"} <= fetched
|
||||||
|
missing = [
|
||||||
|
p
|
||||||
|
for p in fetched
|
||||||
|
if not p.startswith("/static/")
|
||||||
|
and not any(r.match(p.rstrip("/") or "/") for r in patterns)
|
||||||
|
and not any(r.startswith(p) for r in routes)
|
||||||
|
]
|
||||||
|
assert not missing, f"console fetches paths the server does not route: {missing}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_files_it_loads_exist():
|
||||||
|
for path in re.findall(r"/static/([\w./-]+\.(?:geojson|json|woff2))", _html()):
|
||||||
|
assert (STATIC / path).is_file(), path
|
||||||
|
|
||||||
|
|
||||||
|
def test_translations_cover_both_languages():
|
||||||
|
tables = _tables(_html())
|
||||||
|
assert len(tables["en"]) > 100
|
||||||
|
assert not tables["en"] - tables["th"], sorted(tables["en"] - tables["th"])
|
||||||
|
assert not tables["th"] - tables["en"], sorted(tables["th"] - tables["en"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_markup_keys_exist():
|
||||||
|
html = _html()
|
||||||
|
used = set(re.findall(r'data-i18n(?:-placeholder|-title|-aria)?="([^"]+)"', html))
|
||||||
|
assert not used - _tables(html)["en"], sorted(used - _tables(html)["en"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_aria_labels_are_translated():
|
||||||
|
for match in re.finditer(r'<[^>]*\saria-label="[^"]+"[^>]*>', _body(_html())):
|
||||||
|
assert "data-i18n-aria" in match.group(0), match.group(0)[:120]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_emoji_in_the_interface():
|
||||||
|
emoji = re.compile("[\U0001f000-\U0001faff\u2600-\u27bf\u2b00-\u2bff\ufe0f]")
|
||||||
|
found = sorted(set(emoji.findall(_html())))
|
||||||
|
assert not found, f"emoji in console.html: {found}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_thai_first_and_river_time():
|
||||||
|
html = _html()
|
||||||
|
assert "if (code.startsWith('th')) return 'th';" in html
|
||||||
|
assert "const TZ = 'Asia/Bangkok';" in html
|
||||||
|
assert "'+07:00'" in html # naive API timestamps are pinned to Bangkok
|
||||||
|
|
||||||
|
|
||||||
|
def test_fonts_are_self_hosted_with_their_licence():
|
||||||
|
html = _html()
|
||||||
|
assert "fonts.googleapis.com" not in html
|
||||||
|
for name in ("inter-latin.woff2", "noto-sans-thai-thai.woff2"):
|
||||||
|
assert f"/static/fonts/{name}" in html
|
||||||
|
assert (STATIC / "fonts" / "inter-OFL.txt").is_file()
|
||||||
|
assert (STATIC / "fonts" / "noto-sans-thai-OFL.txt").is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_preview_is_not_indexed():
|
||||||
|
assert '<meta name="robots" content="noindex">' in _html()
|
||||||
Reference in new issue
Block a user