frugal-iot-server 2.0.6 → 2.0.8

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/package.json CHANGED
@@ -12,17 +12,17 @@
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.6",
16
- "frugal-iot-logger": "^2.0.6",
15
+ "frugal-iot-client": "^2.0.8",
16
+ "frugal-iot-logger": "^2.0.8",
17
17
  "hash-wasm": "^4.12.0",
18
18
  "js-yaml": "^4.2.0",
19
19
  "morgan": "^1.11.0",
20
20
  "mqtt": "^5.15.2",
21
21
  "multer": "^2.1.1",
22
- "nodemailer": "^9.0.6",
22
+ "nodemailer": "^10.1.2",
23
23
  "passport": "^0.7.0",
24
24
  "passport-local": "^1.0.0",
25
- "sqlite3": "^5.1.7"
25
+ "sqlite3": "^6.0.1"
26
26
  },
27
27
  "bin": {
28
28
  "frugal-iot-server": "frugal-iot-server.js",
@@ -35,7 +35,9 @@
35
35
  "frugal-iot-addbridge-pi": "scripts/addbridge-pi.zsh",
36
36
  "frugal-iot-rebuild-dynsec": "scripts/rebuild-dynsec.js",
37
37
  "frugal-iot-dynsec-init": "scripts/dynsec-init.js",
38
- "frugal-iot-resetnode": "scripts/resetnode.js"
38
+ "frugal-iot-resetnode": "scripts/resetnode.js",
39
+ "frugal-iot-grafana-token": "scripts/grafana-token.js",
40
+ "frugal-iot-backfill-opensearch": "scripts/backfill-opensearch.js"
39
41
  },
40
42
  "description": "Frugal IoT client and server",
41
43
  "//files": "An allowlist, not an ignore list - anything not named here is never published, so a stray file (a database backup, a credential) cannot leak by default. Check with: npm pack --dry-run",
@@ -46,12 +48,14 @@
46
48
  "config.d/email.yaml",
47
49
  "config.d/logger.yaml",
48
50
  "config.d/mqtt.yaml",
51
+ "config.d/opensearch.yaml",
49
52
  "config.d/server.yaml",
50
53
  "config.d/schema/",
51
54
  "lib/",
52
55
  "public/",
53
56
  "private/",
54
57
  "scripts/",
58
+ "extras/grafana/",
55
59
  "extras/mosquitto.conf",
56
60
  "extras/aclfile",
57
61
  "extras/mosquitto-bridge.conf.example",
@@ -60,13 +64,13 @@
60
64
  "extras/frugaliot-ap-watchdog.service",
61
65
  "extras/frugaliot-ap-watchdog.timer",
62
66
  "API.md",
67
+ "GRAFANA.md",
68
+ "INSTALL-OPENSEARCH.md",
63
69
  "INSTALLATION.md"
64
70
  ],
65
71
  "devDependencies": {
66
- "@types/jest": "^30.0.0",
67
- "jest": "^30.4.2",
68
72
  "supertest": "^7.2.2",
69
- "vitest": "^1.0.0"
73
+ "vitest": "^4.1.12"
70
74
  },
71
75
  "homepage": "https://github.com/mitra42/frugal-iot-server",
72
76
  "keywords": [
@@ -89,5 +93,8 @@
89
93
  "check-files": "node scripts/check-published-files.js"
90
94
  },
91
95
  "type": "module",
92
- "version": "2.0.6"
96
+ "version": "2.0.8",
97
+ "overrides": {
98
+ "uuid": "^11.1.1"
99
+ }
93
100
  }
@@ -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.6';
9
+ const CACHE_NAME = 'frugal-iot-cache-2.0.8';
10
10
  /*
11
11
  * Everything the app needs, so that a phone on a poor or expensive link fetches it once.
12
12
  *
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Load an organization's recorded readings into OpenSearch - the history from before its OpenSearch
4
+ * forwarder was switched on, or a gap from while OpenSearch was down.
5
+ *
6
+ * npx --no frugal-iot-backfill-opensearch <org> everything
7
+ * npx --no frugal-iot-backfill-opensearch <org> 2026-09-01 only files from that day on
8
+ * npx --no frugal-iot-backfill-opensearch <org> dry-run count, and send nothing
9
+ *
10
+ * Run from the server's own directory. It uses the organization's OpenSearch forwarder as set up on
11
+ * the dashboard's Forwarders card (a switched-off one is not used), and its "Only these topics" list.
12
+ * Safe to repeat, and to run while the server is: a reading's id comes from its topic and time, so one
13
+ * already in OpenSearch is replaced rather than doubled.
14
+ *
15
+ * Positional arguments, no flags: npm swallows any --flag it does not recognise. See the npx note in
16
+ * claude.md.
17
+ */
18
+
19
+ import sqlite3 from 'sqlite3';
20
+ import { MqttLogger, backfillOpenSearch } from 'frugal-iot-logger';
21
+ import { initializeSchema } from '../lib/database.js';
22
+ import { enabledConfigs } from '../lib/forwarders.js';
23
+
24
+ const [org, ...rest] = process.argv.slice(2);
25
+ const dryRun = rest.includes('dry-run');
26
+ const since = rest.find((a) => /^\d{4}-\d{2}-\d{2}$/.test(a));
27
+ if (!org || rest.some((a) => a !== 'dry-run' && a !== since)) {
28
+ console.error('Usage: npx --no frugal-iot-backfill-opensearch <org> [YYYY-MM-DD] [dry-run]');
29
+ process.exit(1);
30
+ }
31
+
32
+ const realLog = console.log;
33
+ console.log = () => {}; // the config reader logs a line per file
34
+ new MqttLogger().readYamlConfig('.', (err, config) => {
35
+ console.log = realLog;
36
+ if (err) { console.error(`Could not read the configuration: ${err.message}`); process.exit(1); }
37
+ if (!(config.organizations || {})[org]) { console.error(`No organization "${org}"`); process.exit(1); }
38
+ const db = new sqlite3.Database('./frugal-iot.db', async (dberr) => {
39
+ if (dberr) { console.error(`Could not open ./frugal-iot.db: ${dberr.message}`); process.exit(1); }
40
+ try {
41
+ await initializeSchema(db); // makes the table if this database is from before it existed
42
+ const fromDb = (await enabledConfigs(db, org, config.opensearch)).filter((c) => c.type === 'opensearch');
43
+ // Still honoured while YAML forwarders are supported, so a server not yet moved over works
44
+ const fromFile = (config.organizations[org].forwarders || []).filter((c) => c && c.type === 'opensearch');
45
+ const forwarders = [...fromDb, ...fromFile];
46
+ if (forwarders.length === 0) {
47
+ console.error(`${org} has no OpenSearch forwarder - add one on the dashboard's Forwarders card first`);
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ for (const fconfig of forwarders) {
52
+ const { sent, skippedFiles } = await backfillOpenSearch({
53
+ config: fconfig, org, dataDir: config.server.datadir, since, dryRun,
54
+ progress: (n) => process.stdout.write(`\r${n} readings`),
55
+ });
56
+ console.log(`\n${dryRun ? 'Would send' : 'Sent'} ${sent} readings to ${fconfig.node}` +
57
+ (skippedFiles ? `; ${skippedFiles} files skipped by its topics list` : ''));
58
+ }
59
+ } catch (e) {
60
+ console.error(e.message);
61
+ process.exitCode = 1;
62
+ } finally {
63
+ db.close();
64
+ }
65
+ });
66
+ });
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * Issue, list or revoke the tokens Grafana uses to read an organization's history (GRAFANA.md).
4
+ *
5
+ * npx --no frugal-iot-grafana-token add <org> <username> <name> prints the token - once, it is not kept
6
+ * npx --no frugal-iot-grafana-token list <org>
7
+ * npx --no frugal-iot-grafana-token revoke <org> <name>
8
+ *
9
+ * Run from the server's own directory. Adding a name that exists replaces its token, which is how
10
+ * one is rotated. A token is read-only, for that one organization, and acts as <username>: it sees
11
+ * what that user may READ, and stops working when they lose access.
12
+ *
13
+ * Positional arguments, no flags: npm swallows any --flag it does not recognise. See the npx note
14
+ * in claude.md.
15
+ */
16
+
17
+ import sqlite3 from 'sqlite3';
18
+ import { initializeSchema } from '../lib/database.js';
19
+ import { addMissingColumns } from '../lib/schema-columns.js';
20
+ import { issueToken, listTokens, revokeToken } from '../lib/grafana.js';
21
+
22
+ const [action, org, ...rest] = process.argv.slice(2);
23
+ const [username, name] = action === 'add' ? rest : [undefined, rest[0]];
24
+ const usage = () => {
25
+ console.error('Usage: npx --no frugal-iot-grafana-token add <org> <username> <name>');
26
+ console.error(' npx --no frugal-iot-grafana-token list <org>');
27
+ console.error(' npx --no frugal-iot-grafana-token revoke <org> <name>');
28
+ process.exit(1);
29
+ };
30
+ if (!['add', 'list', 'revoke'].includes(action) || !org || (action === 'add' && !username) || (action !== 'list' && !name)) usage();
31
+ if (name && !/^[A-Za-z0-9][A-Za-z0-9 _.-]{0,39}$/.test(name)) {
32
+ console.error('The name must be 1-40 characters: letters, digits, space, _ . -');
33
+ process.exit(1);
34
+ }
35
+
36
+ const db = new sqlite3.Database('./frugal-iot.db', async (err) => {
37
+ if (err) { console.error(`Could not open ./frugal-iot.db: ${err.message}`); process.exit(1); }
38
+ try {
39
+ await initializeSchema(db); // adds the table to a database from before it existed; changes nothing else
40
+ await new Promise((res, rej) => addMissingColumns(db, (e) => (e ? rej(e) : res())));
41
+ if (action === 'add') {
42
+ const user = await new Promise((res, rej) => db.get('SELECT id FROM users WHERE username = ?', [username], (e, r) => (e ? rej(e) : res(r))));
43
+ if (!user) throw new Error(`No user "${username}"`);
44
+ const token = await issueToken(db, org, name, user.id);
45
+ console.log(`Token for ${org} "${name}" (acting as ${username}) - shown once, put it in Grafana now:\n\n${token}\n`);
46
+ } else if (action === 'list') {
47
+ const rows = await listTokens(db, org);
48
+ if (rows.length === 0) console.log(`No Grafana tokens for ${org}`);
49
+ for (const r of rows) {
50
+ console.log(`${r.name}\tissued ${new Date(r.created_at).toISOString()}\tlast used ${r.last_used ? new Date(r.last_used).toISOString() : 'never'}`);
51
+ }
52
+ } else {
53
+ const done = await revokeToken(db, org, name);
54
+ console.log(done ? `Revoked "${name}" for ${org}` : `No token "${name}" for ${org}`);
55
+ if (!done) process.exitCode = 1;
56
+ }
57
+ } catch (e) {
58
+ console.error(e.message);
59
+ process.exitCode = 1;
60
+ }
61
+ db.close();
62
+ });
@@ -571,10 +571,11 @@ PACKAGES=(nodejs npm sqlite3 zsh curl)
571
571
  sudo_ env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}"
572
572
  NODE_VERSION="$(node -v)"
573
573
  info "node ${NODE_VERSION}, npm $(npm -v), sqlite3 $(sqlite3 --version | awk '{print $1}')"
574
- # The server needs node 18 or later
574
+ # The server needs node 20.17 or later (sqlite3 6 needs 20.17, nodemailer 10 needs 20)
575
575
  NODE_MAJOR="$(echo "${NODE_VERSION#v}" | cut -d. -f1)"
576
- if (( NODE_MAJOR < 18 )); then
577
- echo "node ${NODE_VERSION} is too old - the server needs 18 or later." >&2
576
+ NODE_MINOR="$(echo "${NODE_VERSION#v}" | cut -d. -f2)"
577
+ if (( NODE_MAJOR < 20 || (NODE_MAJOR == 20 && NODE_MINOR < 17) )); then
578
+ echo "node ${NODE_VERSION} is too old - the server needs 20.17 or later." >&2
578
579
  echo "Install a current version from https://github.com/nodesource/distributions and run this again." >&2
579
580
  exit 1
580
581
  fi
@@ -1,54 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Frugal IoT Demo</title>
6
- <link href="./frugaliot.css" rel="stylesheet" type="text/css">
7
- <!-- this maps files that webcomponents uses, -->
8
- <script type="importmap">
9
- {
10
- "imports": {
11
- "@kurkle/color": "https://frugaliot.naturalinnovation.org/node_modules/@kurkle/color/dist/color.esm.js",
12
- "chart.js": "https://frugaliot.naturalinnovation.org/node_modules/chart.js/dist/Chart.js",
13
- "dial-gauge": "/node_modules/dial-gauge/dial-gauge.js",
14
- "luxon": "https://frugaliot.naturalinnovation.org/node_modules/luxon/src/luxon.js",
15
- "chartjs-adapter-luxon": "https://frugaliot.naturalinnovation.org/node_modules/chartjs-adapter-luxon/dist/chartjs-adapter-luxon.esm.js",
16
- "csv-parse": "https://frugaliot.naturalinnovation.org/node_modules/csv-parse/dist/esm/index.js"
17
- }
18
- }
19
- </script>
20
- <!-- load webcomponents.js either from local server, or from frugaliot.naturalinnovation.org -->
21
- <script src="./webcomponents.js" type="module"></script>
22
- <!--<script src="./webcomponents.js" type="module"></script>-->
23
- </head>
24
- <body>
25
- <h1>This is an example of embedding a widget in a page</h1>
26
- <div style="display:none">
27
- <!-- every embedded page needs a mqtt-client, it sets up the MQTT connection to the broker -->
28
- <mqtt-client server="wss://frugaliot.naturalinnovation.org/wss"></mqtt-client>
29
- </div>
30
- <div style="float:right;border:2px,solid,blue;width:50%">
31
- <!-- this is a simple display widget - in this case a meter bar -->
32
- <mqtt-bar max="45" min="0" topic="dev/lotus/esp8266-fb94bb/sht/temperature" label="Temperature" type="float" color="red"></mqtt-bar>
33
- <!--<mqtt-bar max="45" min="0" topic="dev/lotus/esp32-70c376/soil" label="Soil" type="float" color="brown"></mqtt-bar>-->
34
- </div>
35
- <div style="float:left;border:2px,solid,blue;width:10%">
36
- <!-- this is a simple control widget - in this case toggles an LED on a device at my office -->
37
- <mqtt-toggle topic="dev/lotus/esp8266-fb94bb/ledbuiltin/on" label="LED" type="bool" color="blue"></mqtt-toggle>
38
- </div>
39
- <p>
40
- This is a demo of a custom web page with a bar widget; a graph; and a control.
41
- These all go to the same device which may or may not always be turned on!
42
- </p>
43
- <p>
44
- <!-- And a pretty simple graph - in this case its displaying the same value from above, but it can
45
- <!-- display any topic -->
46
- <!-- Note - I think there is an issue with back button if loaded from a different server, will fix -->
47
- <mqtt-graph>
48
- <mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/sht/temperature" color="red" label="Office Temperature" min=0 max=50 type="float"></mqtt-graphdataset>
49
- <mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/sht/humidity" color="blue" label="Office Humidity" min=0 max=100 type="float"></mqtt-graphdataset>
50
- <mqtt-graphdataset topic="dev/lotus/esp8266-fb94bb/ledbuiltin/on" color="orange" label="LED" type="bool"></mqtt-graphdataset>
51
- </mqtt-graph>
52
- </p>
53
- </body>
54
- </html>