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/GRAFANA.md +114 -0
- package/INSTALL-OPENSEARCH.md +415 -0
- package/INSTALLATION.md +3 -3
- package/README.md +1 -1
- package/config.d/opensearch.yaml +23 -0
- package/extras/grafana/frugaliot-readings.json +166 -0
- package/frugal-iot-createdb.sql +38 -0
- package/frugal-iot-server.js +47 -5
- package/lib/admin-guards.js +26 -0
- package/lib/config-for-user.js +10 -0
- package/lib/destinations.js +121 -0
- package/lib/forwarders.js +312 -0
- package/lib/grafana.js +420 -0
- package/lib/opensearch-access.js +245 -0
- package/lib/schema-columns.js +1 -0
- package/package.json +16 -9
- package/public/service-worker.js +1 -1
- package/scripts/backfill-opensearch.js +66 -0
- package/scripts/grafana-token.js +62 -0
- package/scripts/install-pi.sh +4 -3
- package/public/index-embedded.html +0 -54
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.
|
|
16
|
-
"frugal-iot-logger": "^2.0.
|
|
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": "^
|
|
22
|
+
"nodemailer": "^10.1.2",
|
|
23
23
|
"passport": "^0.7.0",
|
|
24
24
|
"passport-local": "^1.0.0",
|
|
25
|
-
"sqlite3": "^
|
|
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.
|
|
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.
|
|
96
|
+
"version": "2.0.8",
|
|
97
|
+
"overrides": {
|
|
98
|
+
"uuid": "^11.1.1"
|
|
99
|
+
}
|
|
93
100
|
}
|
package/public/service-worker.js
CHANGED
|
@@ -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.
|
|
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
|
+
});
|
package/scripts/install-pi.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
577
|
-
|
|
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>
|