frugal-iot-server 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/GRAFANA.md CHANGED
@@ -17,7 +17,31 @@ Grafana reads the server with its free **Infinity** plugin, which can call any U
17
17
  > Grafana's API), and the Grafana card on the dashboard clicked through by hand. Correct this page
18
18
  > where they differ.
19
19
 
20
+ ## Installing
21
+
22
+ #### On a Mac,
23
+
24
+ ```sh
25
+ brew install grafana
26
+ brew services start grafana
27
+ ```
28
+
29
+ Open <http://localhost:3000>. Grafana's default login is `admin` / `admin`, and it asks you to change it.
30
+ Then add the Infinity plugin - in Grafana, Administration → Plugins → "Infinity" - or from the shell:
31
+
32
+ ```sh
33
+ /opt/homebrew/opt/grafana/bin/grafana cli --homepath /opt/homebrew/opt/grafana/share/grafana \
34
+ --pluginsDir /opt/homebrew/var/lib/grafana/plugins plugins install yesoreyeram-infinity-datasource
35
+ brew services restart grafana
36
+ ```
37
+ To stop it: `brew services stop grafana` (Homebrew restarts it at login until you do).
38
+
39
+ #### On Windows, Linux and other systems
40
+
41
+ For other systems, Grafana's own download page has installers for [Windows, Linux and others](https://grafana.com/grafana/download).
42
+
20
43
  ## Setting it up
44
+ Access Grafana from your browser at [localhost:3000](http://localhost:3000)
21
45
 
22
46
  Anyone who can **read** the organization can make a token for themselves - you do not need to be an
23
47
  administrator. A token is yours: Grafana sees what *you* can see, so a token made by someone who can
@@ -26,22 +50,29 @@ read only one project shows that project only.
26
50
  1. **Install the Infinity plugin in Grafana.** In Grafana: Administration → Plugins, search for
27
51
  "Infinity", install it, and restart Grafana if it asks. (Installing Grafana itself is in
28
52
  [INSTALL-OPENSEARCH.md](INSTALL-OPENSEARCH.md).)
29
- 2. **Make a token.** In the dashboard, open the project's back, then the **Grafana** card. Type a name
53
+ 2. **Make a token.** In the Frugal IoT dashboard, open the project's back, then the **Grafana** card. Type a name
30
54
  for this Grafana (so it can be revoked on its own later) and press *Make a token*. The token is shown
31
55
  **once** - only a hash is kept, so it cannot be looked up again. If you lose it, make one with the
32
56
  same name, which replaces it.
33
- 3. **Give Grafana the data source.** Either press *Download the data source file* in the same place and
34
- put it in Grafana's `provisioning/datasources` folder, then restart Grafana; or add it by hand in
57
+ 3. **Give Grafana the data source.** Either:
58
+ a: press *Download the data source file* in the same place and
59
+ put it in Grafana's `provisioning/datasources` folder (On a Mac typically in /opt/homebrew/Cellar/grafana/13.3.0/share/grafana/conf/provisioning),
60
+ then restart Grafana:
61
+ - On Mac: `brew services restart grafana`;
62
+
63
+ or add it by hand in
35
64
  Grafana under Connections → Data sources → Add → **Infinity**:
36
65
  - Authentication: **Bearer Token**, with the token
37
66
  - Allowed hosts: the address you reached this dashboard at, e.g. `https://frugaliot.naturalinnovation.org`
38
- 4. **Import the dashboard.** In the Grafana card press *Download the dashboard*. In Grafana choose
39
- Dashboards → New → Import, upload the file, and pick the data source from step 3 when it asks.
67
+ 4. **Import the dashboard.**
68
+ * On FrugalIoT: In the Grafana card press *Download the dashboard*.
69
+ * Then in Grafana choose
70
+ Dashboards → New → Import, upload the file, and pick the data source from step 3 when it asks.
40
71
 
41
72
  The dashboard has a list of every topic at the top to choose from, and the time range at the top right.
42
73
  The file already has this server and organization filled in.
43
74
 
44
- A token lets one Grafana read **one organization**, read-only, and nothing else - and only the
75
+ A token lets one Grafana read **one organization**, read-only - and only the
45
76
  projects of it that the person who made the token may read. For another
46
77
  organization, make its own token, data source and import - the dashboard file is separate for each, so
47
78
  importing a second does not replace the first. *Revoke* on the card cuts off a Grafana at once.
@@ -1,25 +1,22 @@
1
1
  # Installing OpenSearch, Grafana and Dashboards
2
2
 
3
3
  This is for **browsing and charting your readings**. All of it is optional - the server and logger work
4
- without any of it, and the readings are always safe in the CSV files under `data/`.
4
+ without any of it, and the readings are always safe in the CSV files under `data/`,
5
+ and the client has its own graphing.
5
6
 
6
7
  ## What the pieces are
7
8
 
8
- OpenSearch is **two programs**, and it matters which is which:
9
+ OpenSearch is **two programs**:
9
10
 
10
11
  - **OpenSearch** is the database. It stores and searches the readings, and listens on port 9200.
11
- - **OpenSearch Dashboards** is a separate web app for searching and charting what is in OpenSearch. It
12
- holds no readings: it asks OpenSearch for them, over port 9200, using one login in its own
12
+ - **OpenSearch Dashboards** is a separate web app for searching and charting what is in OpenSearch.
13
+ It access readings from OpenSearch at port 9200, using one login in its own
13
14
  configuration file. (It also keeps its own saved dashboards *inside* OpenSearch, in an index.)
14
15
 
15
- **Grafana** is a third, unrelated program that makes charts. It can read the server directly
16
- ([GRAFANA.md](GRAFANA.md)), needing no OpenSearch at all.
17
-
18
16
  ## The ways to use it
19
17
 
20
18
  | | Readings are kept in | You look at them with | Set up |
21
19
  |---|---|---|---|
22
- | **Grafana, reading the server** *(start here)* | the server's CSV files, all of them | Grafana on your computer | [GRAFANA.md](GRAFANA.md); needs no OpenSearch |
23
20
  | **(a) OpenSearch on the server** | OpenSearch on the production server | OpenSearch Dashboards on **your own computer** | sections 2, 3, 4, 5 |
24
21
  | **(b) OpenSearch on your own machine** | OpenSearch on a machine you run, which the server sends readings to | whatever you like - you run it all | section 6 |
25
22
 
@@ -28,9 +25,8 @@ Either of (a) and (b) can be used with Grafana too.
28
25
  **None of this is for the Raspberry Pi bridge.**
29
26
 
30
27
  > **Status:** the Grafana and Mac steps were followed on macOS with Homebrew (OpenSearch 3.9.0,
31
- > Dashboards 3.9.0, Grafana 13.3.0). The Linux steps, the logins for Dashboards, and the Apache setup
32
- > are written from OpenSearch's documentation and have **not been run on a Linux server** - correct them
33
- > where they differ. [Open questions](#open-questions) lists what to check first.
28
+ > Dashboards 3.9.0, Grafana 13.3.0). The Linux steps, the logins for Dashboards, and the Apache setup
29
+ > have been run on the production server at frugaliot.naturalinnovation.org
34
30
 
35
31
  We use **OpenSearch**, not Elasticsearch: it is Apache-licensed, installs with Homebrew and apt, and
36
32
  Grafana can read it. OpenSearch Dashboards is its fork of Kibana, so Kibana guides are close but not
@@ -38,117 +34,122 @@ identical. (The reasoning is also in a comment above the `OpenSearch` class in f
38
34
  `index.js`, in case the decision is revisited.)
39
35
 
40
36
  How heavy is each? OpenSearch is the heavy one: a Java program with its own 1 GB heap by default.
41
- Grafana and Dashboards are small by comparison (about 60 MB and 20 MB resident on a Mac, which
37
+ Dashboards are small by comparison (about 60 MB and 20 MB resident on a Mac, which
42
38
  under-reports, so read that loosely).
43
39
 
44
- ---
45
-
46
- ## 1. On your own computer: Grafana
40
+ ## 1. (a) On the server: OpenSearch
47
41
 
48
- On a Mac:
42
+ **Debian or Ubuntu.**
49
43
 
44
+ **First, check there is room.** OpenSearch with its default 1 GB heap takes about 1.4 GB of RAM in
45
+ total (measured on a 2.4 GB server, where it became by far the biggest process). Run this *before*
46
+ installing:
50
47
  ```sh
51
- brew install grafana
52
- brew services start grafana
48
+ free -h
53
49
  ```
50
+ The `available` column is what is left for OpenSearch; you want at least 1.5 GB. If you have less, either add
51
+ memory or give OpenSearch a smaller heap (the step before starting it, below). Run it again after
52
+ OpenSearch is started and `available` will, of course, be much lower; `top` (press `M` to sort by
53
+ memory, `q` to quit) shows what is using it.
54
54
 
55
- Open <http://localhost:3000>. Grafana's default login is `admin` / `admin`, and it asks you to change it.
56
- Then add the Infinity plugin - in Grafana, Administration → Plugins → "Infinity" - or from the shell:
57
-
55
+ Install the prerequisites
58
56
  ```sh
59
- /opt/homebrew/opt/grafana/bin/grafana cli --homepath /opt/homebrew/opt/grafana/share/grafana \
60
- --pluginsDir /opt/homebrew/var/lib/grafana/plugins plugins install yesoreyeram-infinity-datasource
61
- brew services restart grafana
57
+ sudo apt-get update && sudo apt-get -y install lsb-release ca-certificates curl gnupg2
62
58
  ```
63
-
64
- For other systems, Grafana's own download page has installers for [Windows, Linux and others](https://grafana.com/grafana/download).
65
-
66
- Then continue at [GRAFANA.md](GRAFANA.md) to make a token and chart your readings.
67
-
68
- To stop it: `brew services stop grafana` (Homebrew restarts it at login until you do).
69
-
70
- ---
71
-
72
- ---
73
-
74
- ## 2. (a) On the server: OpenSearch
75
-
76
- Debian or Ubuntu.
77
-
78
- `apt-get` does the installing. The `curl` line just downloads the signing key for OpenSearch's
79
- repository, which apt needs in order to trust it:
80
-
59
+ Fetch the signing key for OpenSearch's repository
81
60
  ```sh
82
- sudo apt-get update && sudo apt-get -y install lsb-release ca-certificates curl gnupg2
83
61
  sudo mkdir -p /etc/apt/keyrings
84
62
  curl -fsSL https://artifacts.opensearch.org/publickeys/opensearch-release.pgp \
85
63
  | sudo gpg --dearmor -o /etc/apt/keyrings/opensearch.gpg
86
64
  ```
65
+ Add OpenSearch's apt repository
66
+ ```sh
67
+ echo "deb [signed-by=/etc/apt/keyrings/opensearch.gpg] https://artifacts.opensearch.org/releases/bundle/opensearch/3.x/apt stable main" \
68
+ | sudo tee /etc/apt/sources.list.d/opensearch-3.x.list
69
+ ```
87
70
 
88
- Then add OpenSearch's apt repository - the `3.x/apt` line from
89
- [their Debian guide](https://docs.opensearch.org/latest/install-and-configure/install-opensearch/debian/),
90
- which is printed there for your architecture - and install, choosing an admin password now:
91
-
71
+ Choose a strong password to replace `<strong password>` with below. Installing does not start it yet.
92
72
  ```sh
93
73
  sudo apt-get update
94
- sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='<choose-a-strong-password>' apt-get install opensearch
74
+ sudo env OPENSEARCH_INITIAL_ADMIN_PASSWORD='<strong password>' apt-get install opensearch
95
75
  sudo systemctl enable opensearch
96
- sudo systemctl start opensearch
97
- curl https://localhost:9200 -u 'admin:<that password>' --insecure
98
76
  ```
99
77
 
100
- (That last `curl` only checks that it is answering. `--insecure` here means "do not check the
101
- certificate", because the one it ships with is self-signed.)
78
+ **Set the memory before starting.** It is set in `/etc/opensearch/jvm.options`, which ships with `-Xms1g`
79
+ and `-Xmx1g` (a 1 GB heap). If `free -h` above showed less than 1.5 GB available, change both lines to
80
+ `512m`, which is enough for a little data: all 598,000 readings of the `dev` organization came to about
81
+ 35 MB. More heap helps with a lot of history, but the machine needs the whole amount free *beyond* what
82
+ the server, broker, database and logger already use. You can change it later, then
83
+ `sudo systemctl restart opensearch`.
84
+
85
+ The other settings, in `/etc/opensearch/opensearch.yml`, need no change.
86
+
87
+ Now start it. This can take a couple of minutes.
88
+ ```sh
89
+ sudo systemctl start opensearch
90
+ ```
91
+ Check it is answering (`--insecure` means "do not check the certificate", because the one it ships with
92
+ is self-signed):
93
+ ```sh
94
+ curl https://localhost:9200 -u 'admin:<strong password>' --insecure
95
+ ```
102
96
 
103
97
  This install **has the security plugin switched on**: HTTPS, a login, and that self-signed certificate.
104
- The password is only used to set up that demo security configuration. Config is in
105
- `/etc/opensearch/opensearch.yml` and the memory setting in `/etc/opensearch/jvm.options`.
98
+ The password is only used to set up that demo security configuration.
106
99
 
107
- - **Memory:** the file ships with `-Xms1g` and `-Xmx1g` (a 1 GB heap). Give both lines the same value;
108
- more helps with a lot of history, and the machine needs that much free *beyond* what the server,
109
- broker and logger use. The data is small: all 598,000 readings of the `dev` organization came to
110
- about 35 MB.
111
100
  - **If it will not start**, look at `journalctl -u opensearch`. A common cause on Linux is the kernel
112
101
  setting `vm.max_map_count` being below 262144; `sudo sysctl -w vm.max_map_count=262144` fixes it until
113
102
  reboot, and a line in `/etc/sysctl.d/` makes it permanent.
114
103
  - **Leave OpenSearch listening on the machine itself** (the default). Only Apache, below, is open to the
115
104
  internet - never port 9200 directly. Check under [Keeping it safe](#keeping-it-safe).
116
105
 
117
- ### Tell the server about it
106
+ ### Tell the Frugal IoT Server about it
118
107
 
119
- Make `config.d/opensearch.yaml` in the server's directory (a commented example ships there), and restart
120
- the server:
108
+ Make `config.d/opensearch.yaml` in the server's directory (a commented example ships there),
121
109
 
122
110
  ```yaml
123
- opensearch:
124
- node: https://localhost:9200 # where THIS server reaches OpenSearch: on this machine
125
- username: admin # what the forwarder writes readings as
126
- password: <the password you chose>
127
- insecure: true # OpenSearch's demo certificate cannot be checked; fine on this machine
128
- admin: # the account that makes the per-organization logins (section 4)
129
- username: admin
130
- password: <the same>
131
- public: https://search.example.org # the address people outside reach it at - section 3
111
+ node: https://localhost:9200 # where THIS server reaches OpenSearch: on this machine
112
+ username: admin # what the forwarder writes readings as
113
+ password: <the password you chose>
114
+ insecure: true # OpenSearch's demo certificate cannot be checked; fine on this machine
115
+ admin: # the account that makes the per-organization logins (section 4)
116
+ username: admin
117
+ password: <the same>
118
+ public: https://search.example.org # the address people outside reach it at - section 3
132
119
  ```
120
+ and restart the Frugal IoT server:
121
+
122
+ ```sh
123
+ sudo service frugaliot restart
124
+ ```
125
+
126
+ The lines go at the top level of the file - the file's name already says they are about `opensearch`.
133
127
 
134
- This file is never sent to a browser. Using `admin` for everything is the simplest thing that works, not
128
+ This file is never sent to a browser. `username:` and `password:` at the top must be an account OpenSearch
129
+ knows - the `admin` one you chose when installing - or every send fails with "Response Error" and the
130
+ logger's journal says so. Using `admin` for everything is the simplest thing that works, not
135
131
  the best: a role that can only write `frugaliot-*` for the forwarder would be better, and is not yet
136
132
  written. Leave `admin:` and `public:` out until you have done sections 3 and 4.
137
133
 
138
134
  ### Send readings to it
139
135
 
140
- In the dashboard, open the project's back, then the **Forwarders** card, and add **OpenSearch on this
136
+ In the dashboard, open the project's back, then the **Forwarders** card (not the **OpenSearch** card), and add **OpenSearch on this
141
137
  server**. There is no address or password to type: they come from the file above. Optionally set *Only
142
138
  these topics*, e.g. `dev/lotus/#`; leave it empty to send everything that is logged.
143
139
 
144
- It applies at once, with no restart, and creates what it needs in OpenSearch (an index template that
140
+ Readings are sent in batches every few minutes, so the first ones take a few minutes to show up. It applies at once, with no restart, and creates what it needs in OpenSearch (an index template that
145
141
  makes `value` a number) the first time it sends. An organization's administrator can add it for their own
146
142
  organization, so only do this if you are happy for them to have their readings in OpenSearch.
147
143
 
148
144
  ### Loading the history you already have
149
145
 
150
- A forwarder only sends readings from the moment it is switched on. To bring in everything already on
151
- disk, from the server's directory (safe to repeat, and safe while the server is running):
146
+ A forwarder only sends readings from the moment it is switched on. To bring in what is already recorded,
147
+ open the **Forwarders** card, and on the OpenSearch forwarder's row press **Send earlier readings...**.
148
+ Pick the day to start from (leave it empty for everything), optionally tick *Only count them* to see how
149
+ many there are first, and press **Start**. It runs in the background and the card shows how many it has
150
+ sent so far. It is safe to repeat, and safe while the server is running.
151
+
152
+ The same thing from a terminal, in the server's directory, if you prefer:
152
153
 
153
154
  ```sh
154
155
  npx --no frugal-iot-backfill-opensearch dev dry-run # count what would be sent
@@ -158,12 +159,22 @@ npx --no frugal-iot-backfill-opensearch dev 2026-09-01 # only files from that d
158
159
 
159
160
  ### Looking after it
160
161
 
162
+ Run these on the server, in a terminal. `admin:<strong password>` is the login and password you chose when
163
+ installing OpenSearch; `--insecure` is the same "don't check the certificate" as before.
164
+
165
+ - **See what is there.** One line per index, with its size:
166
+ ```sh
167
+ curl -s 'https://localhost:9200/_cat/indices/frugaliot-*?v' -u 'admin:<strong password>' --insecure
168
+ ```
161
169
  - **Old months.** Readings go into one index per organization per month, named like
162
- `frugaliot-dev-2026.09`. To drop a month: `curl -X DELETE https://localhost:9200/frugaliot-dev-2026.02 -u admin:... --insecure`.
163
- The CSV files are untouched.
170
+ `frugaliot-dev-2026.09` (that name is in the first column above). To drop one:
171
+ ```sh
172
+ curl -X DELETE 'https://localhost:9200/frugaliot-dev-2026.02' -u 'admin:<strong password>' --insecure
173
+ ```
174
+ The CSV files are untouched, so nothing is lost for good.
164
175
  - **A gap while OpenSearch was down.** The forwarder does not retry a batch it could not send; the CSV
165
- files have everything, and the backfill above fills the gap.
166
- - **Starting afresh.** Delete the indices (`frugaliot-*`) and run the backfill again.
176
+ files have everything, and *Send earlier readings* (above) fills the gap.
177
+ - **Starting afresh.** Delete each index listed above, as for an old month, and send the earlier readings again.
167
178
 
168
179
  ---
169
180
 
@@ -171,15 +182,25 @@ npx --no frugal-iot-backfill-opensearch dev 2026-09-01 # only files from that d
171
182
 
172
183
  ## 3. (a) Apache: HTTPS in front of OpenSearch
173
184
 
174
- Dashboards on someone's computer reaches OpenSearch across the internet, so its login and the readings
175
- must travel over HTTPS. OpenSearch itself stays on the machine (port 9200, loopback); **Apache is put
176
- in front of OpenSearch**, not in front of Dashboards. It is the same Apache that already sits in front
177
- of this server - add one more site to it, on its own name, e.g. `search.example.org`.
185
+ Dashboards on someone's computer reach OpenSearch across the internet, it needs HTTPS.
186
+
187
+ While opensearch ships with HTTP and some self-signed certificates, for most practical installs it will be behind an Apache proxy.
178
188
 
189
+ We'll add a domain name to it - e.g. `opensearch.naturalinnovation.org`
190
+
191
+ Add components needed for Apache
179
192
  ```sh
180
193
  sudo a2enmod ssl proxy proxy_http headers
181
194
  ```
182
195
 
196
+ You need a DNS record pointing `search.example.org` (use your own name) at this server, and a
197
+ certificate for it, made the way you made the server's own, e.g. `sudo certbot certonly --apache -d search.example.org`.
198
+
199
+ Your certificate may be a wildcard one (e.g. `*.example.org`) you already have; then use its files in
200
+ the two `SSLCertificate...` lines below.
201
+
202
+ Make a new file for this site, `/etc/apache2/sites-available/opensearch.conf` (Debian and Ubuntu keep
203
+ every Apache site in that folder; it is separate from your existing site's file), with:
183
204
  ```apache
184
205
  <VirtualHost *:443>
185
206
  ServerName search.example.org
@@ -205,10 +226,17 @@ sudo a2enmod ssl proxy proxy_http headers
205
226
  </LocationMatch>
206
227
  </VirtualHost>
207
228
  ```
229
+ Switch the site on, then test and reload Apache:
230
+ ```sh
231
+ sudo a2ensite opensearch
232
+ sudo apachectl configtest && sudo systemctl reload apache2
233
+ ```
234
+ Check from your own computer:
235
+ `curl https://search.example.org` should answer 401 (it wants a login) - not a block of JSON, or HTML, and not a
236
+ timeout.
208
237
 
209
- Then `sudo apachectl configtest && sudo systemctl reload apache2`. Check from your own computer:
210
- `curl https://search.example.org` should answer 401 (it wants a login) - not a block of JSON, and not a
211
- timeout. Put the address in `public:` in `config.d/opensearch.yaml`.
238
+ Put the address (with the `https://`) in `public:` in `config.d/opensearch.yaml`, with the `admin:` lines
239
+ from section 1, then restart the server so it reads the file: `sudo systemctl restart frugaliot`.
212
240
 
213
241
  Port 9200 itself stays closed to the network (firewall) - only Apache's 443 is open.
214
242
 
@@ -256,17 +284,80 @@ on a Mac), start it (`brew services start opensearch-dashboards`) and open <http
256
284
 
257
285
  ```yaml
258
286
  opensearch.hosts: [ "https://search.example.org" ]
259
- opensearch.username: "frugaliot_dev"
287
+ opensearch.username: "frugaliot_dev_mitra"
260
288
  opensearch.password: "..."
261
289
  opensearchDashboards.index: ".kibana_dev"
262
- opensearch.requestHeadersAllowlist: [ authorization ]
290
+ opensearch.requestHeadersAllowlist: []
291
+ opensearch.customHeaders: { authorization: "Basic <the login, encoded - the card writes this line for you>" }
263
292
  ```
264
293
 
265
294
  The `opensearchDashboards.index` line is what gives the organization its own place to keep saved
266
- dashboards; without it Dashboards uses `.kibana`, which this login cannot touch. Then, in Dashboards:
267
- **Dashboards Management → Index patterns → Create**, enter `frugaliot-*`, and choose `@timestamp` as the
295
+ dashboards; without it Dashboards uses `.kibana`, which this login cannot touch. The last two lines stop Dashboards asking your browser for a login: Dashboards uses the login above
296
+ only for its own housekeeping, so they add the same login to every request. Copy the whole block from
297
+ the card - the `Basic ...` line is the username and password encoded, which you cannot easily write by
298
+ hand. Nothing is typed when you open Dashboards. Keep Dashboards on your own computer: it listens on
299
+ `localhost` by default, and anyone who can reach its port acts as this login.
300
+
301
+ Open opensearch - by default it will be on `http://localhost:5601`
302
+
303
+ Then, in Dashboards:
304
+ **Dashboards Management → Index patterns → Create**, enter `frugaliot-<org>-*` (e.g. `frugaliot-dev-*`; your login can read only your own organization's), and choose `@timestamp` as the
268
305
  time field.
269
306
 
307
+ If it says the pattern **"doesn't match any indices"**, OpenSearch has no readings for your organization yet:
308
+ the forwarder (section 2, "Send readings to it") is not added, or nothing has been sent. Readings only
309
+ arrive once the forwarder is on, so send the earlier ones as in "Loading the history you already have".
310
+ You can check from your own computer with the login that is in the Dashboards file - the generated one the card
311
+ showed (not the OpenSearch `admin` password). This reads it from the file on a Mac; use your own address
312
+ and organization:
313
+ ```sh
314
+ curl -s -u "$(awk -F'"' '/^opensearch.username/{u=$2} /^opensearch.password/{p=$2} END{print u":"p}' /opt/homebrew/etc/opensearch-dashboards/opensearch_dashboards.yml)" 'https://search.example.org/frugaliot-dev-*/_count'
315
+ ```
316
+ `"count":0` with `"total":0` shards means none are there; a number above 0 means they are.
317
+
318
+ If it says **"OpenSearch Dashboards server is not ready yet"**, look at its log
319
+ (`/opt/homebrew/var/log/opensearch-dashboards.log` on a Mac): it names what it could not do. A
320
+ `security_exception ... no permissions for [...]` line means the organization's role on the server is
321
+ missing that permission - press **Replace the password** on the OpenSearch card once (the server rewrites
322
+ the role), then restart Dashboards. A stalled "Another OpenSearch Dashboards instance appears to be
323
+ migrating" means a first start was interrupted; as the message says, delete the index it names
324
+ (`.kibana_<org>_1`, see "Looking after it" for the command) and restart it.
325
+
326
+ ### Seeing your readings as a chart
327
+
328
+ Checked step by step against OpenSearch Dashboards 3.9 in a browser. Each reading has these fields:
329
+ `value` (the number), `@timestamp`, and `topic` split into `node` (the device), `module` and `leaf`
330
+ (e.g. `temperature`). First look at **Discover** (☰ menu, top left): with `frugaliot-<org>-*` chosen you
331
+ see a bar chart of how many readings arrived, and the readings below it.
332
+
333
+ Now a chart of one kind of reading, a line per device:
334
+
335
+ 1. Go to **Visualize** (<http://localhost:5601/app/visualize>) and press **Create new visualization**.
336
+ Choose **Line**, then your `frugaliot-<org>-*` index pattern.
337
+ 2. At the top right, click the calendar icon beside the time and choose **Last 24 hours**. The default
338
+ is the last 15 minutes, which is nearly always empty.
339
+ 3. In the right-hand panel, under **Metrics**, open **Y-axis**. Set **Aggregation** to **Average**
340
+ (type "Average" and pick it). **Field** fills itself in with `value`.
341
+ 4. Under **Buckets** press **Add**, choose **X-axis**, and set **Aggregation** to **Date Histogram**
342
+ (**Field** fills itself in with `@timestamp`). Press **Update** at the bottom right.
343
+ You now have one line, but it averages every kind of reading together (temperature, humidity,
344
+ wifi...), so the numbers mean nothing.
345
+ 5. In the search bar above the chart type `leaf:temperature` and press Enter. Now only temperatures
346
+ are charted.
347
+ 6. Under **Buckets** press **Add** again, choose **Split series**, set **Sub aggregation** to **Terms**
348
+ and **Field** to `node`. Press **Update**. You get one line per device, with its name in the legend.
349
+ 7. Press **Save** (top right), give it a title such as `Temperature by device`, and **Save**.
350
+
351
+ Put it on a dashboard:
352
+
353
+ 1. Go to **Dashboards** (<http://localhost:5601/app/dashboards>) and **Create new dashboard**.
354
+ 2. Press **Add** (top right), click your chart in the list, and close the list with the **X**.
355
+ 3. Press **Save**, give it a title, switch on **Store time with dashboard** (otherwise it opens on the
356
+ last 15 minutes and looks empty), and **Save**.
357
+
358
+ To chart something else, change `temperature` in step 5 to `humidity` or another `leaf`, or leave the
359
+ search empty and split by `topic` instead of `node`.
360
+
270
361
  You can use the same login in **Grafana** instead, with its OpenSearch data source and the same address.
271
362
 
272
363
  ---
@@ -368,8 +459,7 @@ and only your own machine can reach it. Make `config.d/opensearch.yaml` in your
368
459
  just:
369
460
 
370
461
  ```yaml
371
- opensearch:
372
- node: http://localhost:9200
462
+ node: http://localhost:9200
373
463
  ```
374
464
 
375
465
  and restart the server. The Forwarders card then offers **OpenSearch on this server**. The Dashboards
@@ -397,11 +487,7 @@ To stop them: `brew services stop opensearch-dashboards opensearch`.
397
487
 
398
488
  Please add what you find:
399
489
 
400
- - **The Linux steps have not been run**: the exact `apt` repository line, and the Apache site above.
401
- - **The role in section 4 is untried.** Its actions are the ones OpenSearch's documentation names for
402
- Dashboards, and may be missing one - symptoms are Dashboards failing to start, or an index pattern that
403
- cannot be created. If so, OpenSearch's log names the refused action; add it to `roleFor` in
404
- `lib/opensearch-access.js`.
490
+ - **The Linux steps have not been run for the opensource-dashboards**: the exact `apt` repository line, and the Apache site above.
405
491
  - **`opensearchDashboards.index`** is meant to give each organization its own saved-objects index. Check
406
492
  that two organizations' Dashboards do not see each other's saved dashboards, and that neither can open
407
493
  the other's `.kibana_<org>`.
@@ -5,19 +5,23 @@
5
5
  # login for OpenSearch Dashboards on the Dashboards access card. Neither shows an address or a password:
6
6
  # they come from here, and never leave this machine.
7
7
  #
8
- # opensearch:
9
- # node: http://localhost:9200 # where this server reaches OpenSearch - on this machine
10
- # username: frugaliot_logger # what the forwarder writes readings as
11
- # password: change-me
12
- # # insecure: true # only for OpenSearch's demo certificate, which no one can check
8
+ # Write the lines at the top level, as below: the file's name already says they are about opensearch.
9
+ #
10
+ # node: https://localhost:9200 # where this server reaches OpenSearch - on this machine.
11
+ # # https on a Linux install (it has a login); http://localhost:9200
12
+ # # only for Homebrew's OpenSearch on a Mac, which has neither
13
+ # username: admin # what the forwarder writes readings as
14
+ # password: change-me
15
+ # insecure: true # OpenSearch's own certificate is self-signed, which can't be checked.
16
+ # # Needed with https://localhost; leave it out for http
13
17
  #
14
- # # The account that makes and removes the per-organization logins on the Dashboards access card.
15
- # # It must be allowed to change security settings, which is much more than the logger needs - so it
16
- # # is separate, and you can leave it out if you would rather make those logins by hand.
17
- # admin:
18
- # username: admin
19
- # password: change-me
18
+ # # The account that makes and removes the per-organization logins on the Dashboards access card.
19
+ # # It must be allowed to change security settings, which is much more than the logger needs - so it
20
+ # # is separate, and you can leave it out if you would rather make those logins by hand.
21
+ # admin:
22
+ # username: admin
23
+ # password: change-me
20
24
  #
21
- # # The address Dashboards on someone's own computer uses to reach OpenSearch: https, through
22
- # # Apache (INSTALL-OPENSEARCH.md), never the port 9200 above.
23
- # public: https://search.example.org
25
+ # # The address Dashboards on someone's own computer uses to reach OpenSearch: https, through
26
+ # # Apache (INSTALL-OPENSEARCH.md), never the port 9200 above.
27
+ # public: https://search.example.org
@@ -101,7 +101,7 @@ import morgan from 'morgan'; // https://www.npmjs.com/package/morgan - http requ
101
101
 
102
102
  // If you are developing comment out the Production line, and uncomment the Development line
103
103
  // Production
104
- import { MqttLogger } from "frugal-iot-logger"; // https://github.com/mitra42/frugal-iot-logger
104
+ import { MqttLogger, backfillOpenSearch } from "frugal-iot-logger"; // https://github.com/mitra42/frugal-iot-logger
105
105
  // Development of Logger
106
106
  // import { MqttLogger } from "../frugal-iot-logger/index.js"; // https://github.com/mitra42/frugal-iot-logger
107
107
 
@@ -1247,7 +1247,7 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
1247
1247
  app.use('/grafana_token', grafana.admin);
1248
1248
 
1249
1249
  // Where each organization's readings are forwarded, edited from the admin tab - lib/forwarders.js
1250
- app.use('/forwarders', createForwardersRouter(db, mqttLogger, () => config.organizations || {}, { loggedInOrFail, can_ADMIN }, { localOpenSearch: () => config.opensearch }));
1250
+ app.use('/forwarders', createForwardersRouter(db, mqttLogger, () => config.organizations || {}, { loggedInOrFail, can_ADMIN }, { localOpenSearch: () => config.opensearch, backfill: backfillOpenSearch, dataDir: config.server.datadir }));
1251
1251
  app.use('/opensearch_access', createAccessRouter(() => config.organizations || {}, { loggedInOrFail, can_READ }, () => config.opensearch));
1252
1252
  console.log("Mounted Grafana routes at /grafana");
1253
1253
 
package/lib/forwarders.js CHANGED
@@ -212,15 +212,17 @@ export function typesFor(localOpenSearch) {
212
212
  // configuration (config.d/opensearch.yaml), so those never pass through the browser or the database.
213
213
  // A local row on a server that no longer has one is left out.
214
214
  export async function enabledConfigs(db, org, localOpenSearch) {
215
- const out = [];
216
- for (const r of (await listRows(db, org)).filter((x) => x.enabled)) {
217
- const config = JSON.parse(r.config);
218
- if (r.type !== 'opensearch-local') { out.push({ type: r.type, ...config }); continue; }
219
- if (!localOpenSearch || !localOpenSearch.node) continue;
220
- const { node, username, password, apiKey, insecure } = localOpenSearch;
221
- out.push({ type: 'opensearch', node, username, password, apiKey, insecure, ...config });
222
- }
223
- return out;
215
+ return (await listRows(db, org)).filter((x) => x.enabled)
216
+ .map((r) => loggerConfig(r, localOpenSearch)).filter(Boolean);
217
+ }
218
+
219
+ // One row as the logger takes it, or undefined for a local row on a server that no longer has one
220
+ export function loggerConfig(row, localOpenSearch) {
221
+ const config = JSON.parse(row.config);
222
+ if (row.type !== 'opensearch-local') return { type: row.type, ...config };
223
+ if (!localOpenSearch || !localOpenSearch.node) return undefined;
224
+ const { node, username, password, apiKey, insecure } = localOpenSearch;
225
+ return { type: 'opensearch', node, username, password, apiKey, insecure, ...config };
224
226
  }
225
227
 
226
228
  /**
@@ -229,8 +231,9 @@ export async function enabledConfigs(db, org, localOpenSearch) {
229
231
  * @param orgs () => the organizations configuration, { org: {...} }
230
232
  * @param auth { loggedInOrFail, can_ADMIN } from frugal-iot-server.js, passed in so tests can stand in
231
233
  * @param localOpenSearch () => the server's own OpenSearch settings, or undefined (config.d/opensearch.yaml)
234
+ * @param backfill the logger's backfillOpenSearch, and dataDir where the readings are (tests stand in)
232
235
  */
233
- export function createForwardersRouter(db, mqttLogger, orgs, auth, { lookup, localOpenSearch = () => undefined } = {}) {
236
+ export function createForwardersRouter(db, mqttLogger, orgs, auth, { lookup, localOpenSearch = () => undefined, backfill, dataDir } = {}) {
234
237
  const router = Router();
235
238
  // Whole-organization ADMIN: a forwarder sends every project's readings
236
239
  const guard = [auth.loggedInOrFail, auth.can_ADMIN, orgWideAdmin];
@@ -308,5 +311,41 @@ export function createForwardersRouter(db, mqttLogger, orgs, auth, { lookup, loc
308
311
  } catch (err) { next(err); }
309
312
  });
310
313
 
314
+ // Sending the readings already on disk to an OpenSearch forwarder. It can take minutes, so the request
315
+ // only starts it; the card asks how it is getting on. One at a time per forwarder, and the last
316
+ // outcome is kept (in memory - it is gone at a restart) for the card to show.
317
+ const jobs = new Map();
318
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
319
+
320
+ router.post('/:org/backfill', ...guard, orgExists, jsonOnly, express.json({ limit: '1kb' }), async (req, res, next) => {
321
+ try {
322
+ const org = req.params.org;
323
+ if (!backfill || !dataDir) return fail(res, 501, 'This server cannot send earlier readings');
324
+ const { id, since, dryRun } = req.body || {};
325
+ if (since !== undefined && since !== '' && (!DAY.test(since) || Number.isNaN(Date.parse(since)) || new Date(since).toISOString().slice(0, 10) !== since)) return fail(res, 400, 'Give the date as YYYY-MM-DD');
326
+ const row = await get(db, 'SELECT * FROM forwarders WHERE id = ? AND org = ?', [id, org]);
327
+ if (!row) return fail(res, 404, 'No such forwarder');
328
+ if (!['opensearch', 'opensearch-local'].includes(row.type)) return fail(res, 400, 'Only OpenSearch forwarders can be sent earlier readings');
329
+ if (!row.enabled) return fail(res, 400, 'Make the forwarder active first');
330
+ const config = loggerConfig(row, localOpenSearch());
331
+ if (!config) return fail(res, 400, 'This server has no OpenSearch of its own any more');
332
+ const key = `${org}:${row.id}`;
333
+ if (jobs.has(key) && jobs.get(key).running) return fail(res, 409, 'Already sending - wait for it to finish');
334
+ const job = { running: true, sent: 0, dryRun: !!dryRun, since: since || null, startedAt: Date.now() };
335
+ jobs.set(key, job);
336
+ backfill({ config, org, dataDir, since: since || undefined, dryRun: !!dryRun, progress: (n) => { job.sent = n; } })
337
+ .then((r) => { job.sent = r.sent; job.skippedFiles = r.skippedFiles; })
338
+ .catch((err) => { job.error = err.message; console.error(`Backfill for ${org} failed:`, err); })
339
+ .finally(() => { job.running = false; job.finishedAt = Date.now(); });
340
+ console.log('Backfill started by', req.user.username, '-', org, JSON.stringify(row.name), since || 'all', dryRun ? 'dry run' : '');
341
+ res.status(202).json({ started: true });
342
+ } catch (err) { next(err); }
343
+ });
344
+
345
+ router.get('/:org/backfill/:id', ...guard, orgExists, (req, res) => {
346
+ res.set('Cache-Control', 'no-store');
347
+ res.json(jobs.get(`${req.params.org}:${req.params.id}`) || { running: false });
348
+ });
349
+
311
350
  return router;
312
351
  }
@@ -60,12 +60,14 @@ export const namesFor = (org, username) => ({
60
60
  export function roleFor(org) {
61
61
  const n = namesFor(org);
62
62
  return {
63
- cluster_permissions: ['cluster_composite_ops', 'cluster:monitor/main', 'cluster:monitor/health'],
63
+ cluster_permissions: ['cluster_composite_ops', 'cluster:monitor/main', 'cluster:monitor/health', 'cluster:monitor/nodes/info', 'cluster:monitor/state'],
64
64
  index_permissions: [
65
65
  { index_patterns: [n.dataPattern],
66
66
  allowed_actions: ['read', 'indices:admin/get', 'indices:admin/mappings/get',
67
67
  'indices:admin/resolve/index', 'indices:monitor/settings/get'] },
68
68
  { index_patterns: [n.savedIndex, `${n.savedIndex}_*`], allowed_actions: ['indices_all'] },
69
+ // Dashboards looks an alias up before it makes one, and OpenSearch checks that against every index
70
+ { index_patterns: ['*'], allowed_actions: ['indices:admin/aliases/get'] },
69
71
  ],
70
72
  };
71
73
  }
@@ -156,7 +158,11 @@ export function dashboardsConfig(cfg, org, username, password) {
156
158
  `opensearch.username: ${JSON.stringify(n.user)}`,
157
159
  `opensearch.password: ${JSON.stringify(password)}`,
158
160
  `opensearchDashboards.index: ${JSON.stringify(n.savedIndex)}`,
159
- 'opensearch.requestHeadersAllowlist: [ authorization ]',
161
+ // Dashboards uses the login above only for its own housekeeping, and leaves it off requests made for a
162
+ // browser - which would then ask for one. A header set here goes on every request and a browser cannot
163
+ // override it, so nobody is asked.
164
+ 'opensearch.requestHeadersAllowlist: []',
165
+ `opensearch.customHeaders: { authorization: ${JSON.stringify('Basic ' + Buffer.from(`${n.user}:${password}`).toString('base64'))} }`,
160
166
  ].join('\n');
161
167
  }
162
168
 
@@ -200,6 +206,7 @@ export function createAccessRouter(orgs, auth, localOpenSearch = () => undefined
200
206
  all: has(req, 'ADMIN') ? all : undefined });
201
207
  } catch (err) {
202
208
  if (err instanceof AccessError) return res.json({ available: false, reason: err.message });
209
+ console.error('OpenSearch Dashboards access: could not reach', cfg.node, '-', err.code || '', err.message);
203
210
  return res.json({ available: false, reason: 'OpenSearch did not answer' });
204
211
  }
205
212
  } catch (err) { next(err); }
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  "cookie-parser": "^1.4.7",
13
13
  "express": "^5.2.1",
14
14
  "express-session": "^1.19.0",
15
- "frugal-iot-client": "^2.0.8",
16
- "frugal-iot-logger": "^2.0.8",
15
+ "frugal-iot-client": "^2.0.9",
16
+ "frugal-iot-logger": "^2.0.9",
17
17
  "hash-wasm": "^4.12.0",
18
18
  "js-yaml": "^4.2.0",
19
19
  "morgan": "^1.11.0",
@@ -93,7 +93,7 @@
93
93
  "check-files": "node scripts/check-published-files.js"
94
94
  },
95
95
  "type": "module",
96
- "version": "2.0.8",
96
+ "version": "2.0.9",
97
97
  "overrides": {
98
98
  "uuid": "^11.1.1"
99
99
  }
@@ -6,7 +6,7 @@
6
6
  // it, an installed browser keeps serving the files it already cached - and since the list now covers
7
7
  // the dashboard's own code, that means old JS behind freshly fetched HTML. Any edit to the list
8
8
  // needs this bumped, or it does not reach anyone who already has the old one.
9
- const CACHE_NAME = 'frugal-iot-cache-2.0.8';
9
+ const CACHE_NAME = 'frugal-iot-cache-2.0.9';
10
10
  /*
11
11
  * Everything the app needs, so that a phone on a poor or expensive link fetches it once.
12
12
  *