diff --git a/.env.example b/.env.example deleted file mode 100644 index 1d0205a..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -BASIC_AUTH_USER=admin -BASIC_AUTH_PASSWORD=change-me -# HEATPUMP_PORT=8080 diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 4c49bd7..0000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.env diff --git a/README.md b/README.md index 4124447..55b982c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ A small static web app that visualises when a heat pump compressor is running, based on Home Assistant history exports (`binary_sensor.*` on/off states). -- Interactive timeline (pan, zoom, mouse-wheel, quick ranges 6 h / 24 h / 3 d / 7 d) +- Interactive timeline (pan, zoom, mouse-wheel, quick ranges 6 h / 24 h / 3 d / 7 d; + double-click or Esc resets the zoom) - Hover an on-phase to see its start, end and duration - Statistics for the visible range: number of phases, min / max / mean / median duration, total on-time, duty cycle, mean pause between phases @@ -20,13 +21,18 @@ Put CSV exports into `data/`. Every `*.csv` in there is baked into the image. - **One file:** it is loaded directly. - **Several files:** a dropdown in the header lists them alphabetically. The - most recently modified file (or the viewer's last choice) is preselected. + viewer's last choice is preselected, otherwise the **last file by name**. + Name files by date (`2026-09.csv`, `2026-10.csv`, …) so the newest comes last. A different CSV can also be opened ad hoc with **Load CSV…**. It is parsed in the browser and not uploaded. The file list comes from nginx's JSON directory listing of `/data/`, so nothing -has to be registered anywhere. Add or remove files and rebuild the image. +has to be registered anywhere. To add or remove files, commit and push; CI +builds a new image. Locally, use `docker compose up -d --build`. + +> The data is part of the image. Anyone who can pull the image (or read this +> repository) can read the CSVs; basic auth only protects the running web app. ### CSV format @@ -76,11 +82,12 @@ the app without a password. ### Local run with Compose ```sh -cp .env.example .env # set BASIC_AUTH_USER / BASIC_AUTH_PASSWORD docker compose up -d --build ``` -Open (port configurable with `HEATPUMP_PORT`). +Open and log in as `dev` / `dev`. These development +credentials are set in `docker-compose.yml`. The host port can be changed with +`HEATPUMP_PORT=9000 docker compose up -d`. ## CI: build and push @@ -102,7 +109,9 @@ Setup: - `REGISTRY_TOKEN`: that robot account's secret 2. A runner with Docker access must be registered for the repo, user or instance (`act_runner` with the Docker socket mounted), and it must provide - the `ubuntu-latest` label. + the `ubuntu-latest` label. The job image needs Node 20 and the Docker CLI, + e.g. `docker.gitea.com/runner-images:ubuntu-latest`. The old default + `node:16-bullseye` fails on `actions/checkout@v4`. ## Project layout diff --git a/data/history2.csv b/data/history2.csv deleted file mode 100644 index 70cb55d..0000000 --- a/data/history2.csv +++ /dev/null @@ -1,71 +0,0 @@ -entity_id,state,last_changed -binary_sensor.heatpump_compressor,on,2026-09-17T03:00:19.068Z -binary_sensor.heatpump_compressor,off,2026-09-17T03:08:01.062Z -binary_sensor.heatpump_compressor,on,2026-09-17T11:26:17.041Z -binary_sensor.heatpump_compressor,off,2026-09-17T11:41:03.220Z -binary_sensor.heatpump_compressor,on,2026-09-18T03:00:22.102Z -binary_sensor.heatpump_compressor,off,2026-09-18T03:19:35.943Z -binary_sensor.heatpump_compressor,on,2026-09-18T10:19:43.304Z -binary_sensor.heatpump_compressor,off,2026-09-18T10:36:39.042Z -binary_sensor.heatpump_compressor,on,2026-09-19T01:20:46.148Z -binary_sensor.heatpump_compressor,off,2026-09-19T01:22:54.097Z -binary_sensor.heatpump_compressor,on,2026-09-19T02:22:51.267Z -binary_sensor.heatpump_compressor,off,2026-09-19T02:28:06.281Z -binary_sensor.heatpump_compressor,on,2026-09-19T03:28:40.219Z -binary_sensor.heatpump_compressor,off,2026-09-19T03:35:46.221Z -binary_sensor.heatpump_compressor,on,2026-09-19T04:41:58.300Z -binary_sensor.heatpump_compressor,off,2026-09-19T04:49:50.417Z -binary_sensor.heatpump_compressor,on,2026-09-19T05:57:54.487Z -binary_sensor.heatpump_compressor,off,2026-09-19T06:43:11.390Z -binary_sensor.heatpump_compressor,on,2026-09-19T09:12:49.606Z -binary_sensor.heatpump_compressor,off,2026-09-19T09:29:53.590Z -binary_sensor.heatpump_compressor,on,2026-09-20T03:39:39.610Z -binary_sensor.heatpump_compressor,off,2026-09-20T03:42:03.650Z -binary_sensor.heatpump_compressor,on,2026-09-20T04:41:55.897Z -binary_sensor.heatpump_compressor,off,2026-09-20T04:47:38.833Z -binary_sensor.heatpump_compressor,on,2026-09-20T05:48:38.700Z -binary_sensor.heatpump_compressor,off,2026-09-20T05:55:45.486Z -binary_sensor.heatpump_compressor,on,2026-09-20T06:00:23.429Z -binary_sensor.heatpump_compressor,off,2026-09-20T06:36:28.105Z -binary_sensor.heatpump_compressor,on,2026-09-20T09:50:15.244Z -binary_sensor.heatpump_compressor,off,2026-09-20T10:01:52.926Z -binary_sensor.heatpump_compressor,on,2026-09-21T03:00:04.587Z -binary_sensor.heatpump_compressor,off,2026-09-21T03:05:11.361Z -binary_sensor.heatpump_compressor,unknown,2026-09-21T08:18:28.438Z -binary_sensor.heatpump_compressor,off,2026-09-21T08:18:30.643Z -binary_sensor.heatpump_compressor,on,2026-09-22T03:00:05.235Z -binary_sensor.heatpump_compressor,off,2026-09-22T03:20:51.957Z -binary_sensor.heatpump_compressor,on,2026-09-22T09:45:24.980Z -binary_sensor.heatpump_compressor,off,2026-09-22T10:05:30.687Z -binary_sensor.heatpump_compressor,on,2026-09-23T03:00:08.414Z -binary_sensor.heatpump_compressor,off,2026-09-23T03:12:46.457Z -binary_sensor.heatpump_compressor,on,2026-09-23T09:45:29.000Z -binary_sensor.heatpump_compressor,off,2026-09-23T10:04:03.857Z -binary_sensor.heatpump_compressor,on,2026-09-24T03:00:09.997Z -binary_sensor.heatpump_compressor,off,2026-09-24T03:11:26.925Z -binary_sensor.heatpump_compressor,on,2026-09-24T12:34:34.499Z -binary_sensor.heatpump_compressor,off,2026-09-24T13:05:30.499Z -binary_sensor.heatpump_compressor,on,2026-09-25T03:00:07.285Z -binary_sensor.heatpump_compressor,off,2026-09-25T03:39:07.324Z -binary_sensor.heatpump_compressor,unknown,2026-09-25T08:35:58.362Z -binary_sensor.heatpump_compressor,off,2026-09-25T08:36:05.755Z -binary_sensor.heatpump_compressor,unknown,2026-09-25T08:36:57.698Z -binary_sensor.heatpump_compressor,off,2026-09-25T08:36:58.846Z -binary_sensor.heatpump_compressor,unknown,2026-09-25T08:41:12.345Z -binary_sensor.heatpump_compressor,off,2026-09-25T08:41:15.133Z -binary_sensor.heatpump_compressor,on,2026-09-25T09:28:41.719Z -binary_sensor.heatpump_compressor,off,2026-09-25T09:54:51.506Z -binary_sensor.heatpump_compressor,on,2026-09-26T00:42:45.260Z -binary_sensor.heatpump_compressor,off,2026-09-26T00:44:28.166Z -binary_sensor.heatpump_compressor,on,2026-09-26T01:18:30.843Z -binary_sensor.heatpump_compressor,off,2026-09-26T01:23:43.918Z -binary_sensor.heatpump_compressor,on,2026-09-26T02:10:50.330Z -binary_sensor.heatpump_compressor,off,2026-09-26T02:18:56.282Z -binary_sensor.heatpump_compressor,on,2026-09-26T03:08:29.828Z -binary_sensor.heatpump_compressor,off,2026-09-26T03:17:03.786Z -binary_sensor.heatpump_compressor,on,2026-09-26T04:07:50.486Z -binary_sensor.heatpump_compressor,off,2026-09-26T04:17:22.353Z -binary_sensor.heatpump_compressor,on,2026-09-26T05:06:08.060Z -binary_sensor.heatpump_compressor,off,2026-09-26T05:16:05.238Z -binary_sensor.heatpump_compressor,on,2026-09-26T06:00:08.484Z -binary_sensor.heatpump_compressor,off,2026-09-26T06:37:50.037Z diff --git a/html/app.js b/html/app.js index 8f9a0fa..a5cb653 100644 --- a/html/app.js +++ b/html/app.js @@ -348,11 +348,11 @@ async function init() { } state.datasets = files.map(f => f.name); - // last selection if still present, otherwise the most recently modified file + // last selection if still present, otherwise the last file by name + // (mtimes are useless here: git checkouts in CI give all files the same time) let saved = null; try { saved = localStorage.getItem(DATASET_KEY); } catch { /* storage unavailable */ } - const newest = files.reduce((a, b) => Date.parse(b.mtime) > Date.parse(a.mtime) ? b : a); - loadDataset(state.datasets.includes(saved) ? saved : newest.name); + loadDataset(state.datasets.includes(saved) ? saved : state.datasets[state.datasets.length - 1]); } init();