frugal-iot-server 0.3.7 → 0.3.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.
@@ -9,6 +9,11 @@
9
9
  # name Capitalized English (and add translation below in 'languages')
10
10
  # max For gauges, slider
11
11
  # min For gauges, slider
12
+ # width Total characters of the rendered number, including any sign and the decimal point and
13
+ # excluding units - the same meaning the node gives it. Decimals are derived, not declared:
14
+ # intWidth = max(len(int(min)), len(int(max))) including a '-', then decimals = width-intWidth-1.
15
+ # Deriving from min/max rather than from the value keeps the decimal count steady as the value
16
+ # moves. Only float and exponential topics need it; an int has no decimals to decide.
12
17
  # color can be a name or a #RRGGBB
13
18
  # display One of bar,gauge,text,slider,inputbox
14
19
  # slot if this topic fits into a specific slot in the enclosing module (currently just frugal_iot module)
@@ -21,7 +26,8 @@
21
26
  # no setting at all, any float, int or bool with "rw: r" is recorded, which is easy to be
22
27
  # surprised by. scripts/check-schema.js warns about a topic that does not say.
23
28
  # duplicates when to bother recording a reading: significantvalue is how much it has to have moved
24
- # since the last one recorded, significantdate how long (in milliseconds) since that one, and a
29
+ # since the last one recorded - either an absolute amount e.g. 0.5, or as a percentage of the last
30
+ # reading e.g. "2%" - significantdate how long (in milliseconds) since that one, and a
25
31
  # reading that meets neither is dropped. Every topic with "log: true" wants a rule here - without
26
32
  # one, every reading is written to the SD card as it arrives, which wears the card out.
27
33
  # Note battery gets special cased
@@ -30,9 +36,11 @@ altitude:
30
36
  leaf: altitude
31
37
  name: Altitude
32
38
  type: float
39
+ units: m
33
40
  display: text
34
41
  min: -500
35
42
  max: 9000
43
+ width: 6
36
44
  color: black
37
45
  log: true
38
46
  duplicates:
@@ -111,6 +119,7 @@ bus:
111
119
  display: text
112
120
  min: 0
113
121
  max: 32
122
+ width: 5
114
123
  color: green
115
124
  rw: r
116
125
  log: true
@@ -142,6 +151,7 @@ controlfloat:
142
151
  #[leaf, name] should be overridden
143
152
  min: 0
144
153
  max: 100
154
+ width: 5
145
155
  type: float
146
156
  log: false
147
157
  display: text
@@ -198,6 +208,7 @@ course:
198
208
  display: text
199
209
  min: 0
200
210
  max: 360
211
+ width: 5
201
212
  color: black
202
213
  log: true
203
214
  duplicates:
@@ -213,6 +224,7 @@ current:
213
224
  display: text
214
225
  min: -3200
215
226
  max: 3200
227
+ width: 7
216
228
  color: red
217
229
  rw: r
218
230
  log: true
@@ -237,6 +249,7 @@ do:
237
249
  display: bar
238
250
  min: 0
239
251
  max: 20
252
+ width: 5
240
253
  color: blue
241
254
  rw: r
242
255
  log: true
@@ -267,6 +280,7 @@ gas:
267
280
  display: text
268
281
  min: 0
269
282
  max: 500
283
+ width: 5
270
284
  color: green
271
285
  rw: r
272
286
  log: true
@@ -282,6 +296,7 @@ hdop:
282
296
  display: text
283
297
  min: 0
284
298
  max: 50
299
+ width: 5
285
300
  color: black
286
301
  log: false
287
302
  duplicates:
@@ -296,6 +311,7 @@ humidity:
296
311
  display: bar
297
312
  min: 0
298
313
  max: 100
314
+ width: 5
299
315
  color: blue
300
316
  log: true
301
317
  duplicates:
@@ -342,6 +358,7 @@ latitude:
342
358
  display: text
343
359
  min: -90
344
360
  max: +90
361
+ width: 9
345
362
  color: black
346
363
  log: true
347
364
  duplicates:
@@ -357,6 +374,7 @@ load:
357
374
  display: text
358
375
  min: 0
359
376
  max: 32
377
+ width: 5
360
378
  color: green
361
379
  rw: r
362
380
  log: true
@@ -372,6 +390,7 @@ loadcell:
372
390
  display: text
373
391
  min: 0
374
392
  max: 65000
393
+ width: 7
375
394
  color: yellow
376
395
  log: true
377
396
  duplicates:
@@ -387,6 +406,7 @@ longitude:
387
406
  display: text
388
407
  min: -180
389
408
  max: +180
409
+ width: 10
390
410
  color: black
391
411
  log: true
392
412
  duplicates:
@@ -402,12 +422,13 @@ lux:
402
422
  display: bar
403
423
  min: 0
404
424
  max: 65000
425
+ width: 5
405
426
  color: yellow
406
427
  log: true
407
428
  rw: r
408
429
  duplicates:
409
430
  significantdate: 900000
410
- significantvalue: 100
431
+ significantvalue: 5%
411
432
  graphable: true
412
433
  units: lx
413
434
  manual:
@@ -442,6 +463,7 @@ mm:
442
463
  display: text
443
464
  min: 0
444
465
  max: 500
466
+ width: 5
445
467
  color: blue
446
468
  rw: r
447
469
  log: true
@@ -499,6 +521,7 @@ power:
499
521
  display: text
500
522
  min: 0
501
523
  max: 100000
524
+ width: 8
502
525
  color: orange
503
526
  rw: r
504
527
  log: true
@@ -512,8 +535,10 @@ pressure:
512
535
  name: Pressure
513
536
  type: float
514
537
  display: bar
515
- min: 0
516
- max: 99
538
+ min: 300
539
+ max: 1100
540
+ width: 6
541
+ units: hPa
517
542
  color: blue
518
543
  rw: r
519
544
  log: true
@@ -528,6 +553,7 @@ rain:
528
553
  display: text
529
554
  min: 0
530
555
  max: 500
556
+ width: 5
531
557
  color: blue
532
558
  rw: r
533
559
  log: true
@@ -543,6 +569,7 @@ satellites:
543
569
  display: text
544
570
  min: 0
545
571
  max: 32
572
+ width: 2
546
573
  color: black
547
574
  log: false
548
575
  duplicates:
@@ -550,7 +577,7 @@ satellites:
550
577
  significantvalue: 1
551
578
  rw: r
552
579
  graphable: false
553
- units: m/s
580
+ units: count
554
581
  select:
555
582
  leaf: select
556
583
  name: Select
@@ -569,6 +596,7 @@ shunt:
569
596
  display: text
570
597
  min: -320
571
598
  max: 320
599
+ width: 6
572
600
  color: blue
573
601
  rw: r
574
602
  log: true
@@ -584,6 +612,7 @@ speed:
584
612
  display: text
585
613
  min: 0
586
614
  max: 999
615
+ width: 5
587
616
  color: black
588
617
  log: true
589
618
  duplicates:
@@ -607,7 +636,7 @@ soil:
607
636
  significantvalue: 1
608
637
  graphable: true
609
638
  calibrate: true
610
- units: "%RH"
639
+ units: "%"
611
640
  temperature:
612
641
  leaf: temperature
613
642
  name: Temperature
@@ -616,6 +645,7 @@ temperature:
616
645
  rw: r
617
646
  min: 0
618
647
  max: 50
648
+ width: 4
619
649
  color: red
620
650
  wireable: false
621
651
  log: true
@@ -629,6 +659,7 @@ timeoff:
629
659
  name: Time Off (s)
630
660
  min: 0
631
661
  max: 3600
662
+ width: 4
632
663
  type: float
633
664
  log: false
634
665
  display: text
@@ -641,6 +672,7 @@ timeon:
641
672
  name: Time On (s)
642
673
  min: 0
643
674
  max: 3600
675
+ width: 4
644
676
  type: float
645
677
  log: false
646
678
  display: text
@@ -670,6 +702,7 @@ ultrasonic:
670
702
  display: bar
671
703
  min: 0
672
704
  max: 7500
705
+ width: 4
673
706
  color: blue
674
707
  rw: r
675
708
  log: true
@@ -0,0 +1,130 @@
1
+ # Bridging this Pi's broker to a production Frugal IoT server
2
+ #
3
+ # Optional. Without this file the Pi is self-contained: nodes publish to the broker here, the
4
+ # logger records the readings here, and the dashboard here shows them. Adding it also relays
5
+ # everything to a production server, so the same nodes appear on a dashboard elsewhere and can be
6
+ # controlled from it, while the Pi carries on working on its own whenever the link is down.
7
+ #
8
+ # This is a Mosquitto "bridge" - the broker's own feature for connecting to another broker. The
9
+ # server, the logger and the nodes know nothing about it and need no configuration change.
10
+ #
11
+ # To use it:
12
+ # sudo cp extras/mosquitto-bridge.conf.example /etc/mosquitto/conf.d/frugal-iot-bridge.conf
13
+ # sudo nano /etc/mosquitto/conf.d/frugal-iot-bridge.conf # replace every <...> below
14
+ # sudo systemctl restart mosquitto
15
+ #
16
+ # It has to be a restart, not a reload: Mosquitto does not pick up bridges on a reload signal.
17
+ # That briefly disconnects every node, which they recover from on their own.
18
+ #
19
+ # The file goes in /etc/mosquitto/conf.d/ rather than in this installation, because it holds a
20
+ # password for the production broker. Everything under config.d/ here is served to any logged-in
21
+ # browser by /config.json, so a credential put there would not stay private.
22
+
23
+ # ============ The connection ============
24
+
25
+ # Names the bridge. Anything, but it appears in the log, so make it recognisable.
26
+ connection to-prod
27
+
28
+ # The production broker's plain-MQTT-over-TLS address. Not the wss:// URL the browsers and the
29
+ # server use - a Mosquitto bridge speaks MQTT or MQTT-over-TLS and cannot use WebSockets, so
30
+ # production needs a listener on 8883 for this. Port 1883 would work on a trusted LAN, but sends
31
+ # the password below in clear, so over the internet use 8883 and keep the TLS lines further down.
32
+ address <prod-host>:8883
33
+
34
+ # The account this Pi uses on the production broker. Give each Pi its own, rather than sharing the
35
+ # organization's account: the organization password is handed to every browser that logs in, so it
36
+ # is not a secret, and a per-Pi account can be restricted to just this site's topics and revoked
37
+ # on its own.
38
+ remote_username <bridge-account>
39
+ remote_password <bridge-password>
40
+
41
+ # The client id this Pi presents to production, and the name production uses when it reports the
42
+ # state of this bridge (see "notifications" below). One per Pi, or two Pis will disconnect each
43
+ # other in a loop as each takes over the other's session.
44
+ remote_clientid <bridge-account>
45
+
46
+ # Verify production's certificate against the system CA store, which is what a Let's Encrypt
47
+ # certificate on production needs - no certificate has to be copied onto this Pi. Delete both
48
+ # lines, and use port 1883 above, only on a network you trust end to end.
49
+ bridge_cafile /etc/ssl/certs/ca-certificates.crt
50
+ bridge_insecure false
51
+
52
+ # How long to wait before retrying, in seconds, doubling from the first number up to the second.
53
+ # A Pi on a weak or intermittent link keeps retrying for as long as it takes; the cap stops a
54
+ # flapping connection from hammering production.
55
+ restart_timeout 5 60
56
+
57
+ # Keep production's copy of our subscriptions across a dropped connection. This is the default,
58
+ # and it matters: with "true" instead, Mosquitto re-subscribes on every reconnect and production
59
+ # re-sends every retained message each time, which on a flaky link means a burst of duplicated
60
+ # readings after every blip. Nodes publish nearly everything retained, so that burst is large.
61
+ cleansession false
62
+
63
+ # Publish a retained "1" or "0" to $SYS/broker/connection/<remote_clientid>/state on both brokers
64
+ # whenever the link comes up or goes down. This is how "npx --no frugal-iot-diagnostic" reports
65
+ # whether the bridge is connected, and it also lets production see which sites are reachable.
66
+ # The default is true; it is written out here because it is useful rather than incidental.
67
+ notifications true
68
+
69
+ # Uncomment if production refuses the bridge's connection or logs a denied publish to $SYS. Once
70
+ # production has an acl_file, writing to $SYS may not be permitted, and this stops trying: the
71
+ # state is then published only to the broker here, which is all the diagnostic script reads. What
72
+ # is lost is production's view of whether this site is reachable.
73
+ #notifications_local_only true
74
+
75
+ # Not set, and worth knowing why: local_username / local_password. The broker here requires
76
+ # authentication, but a bridge's local side is internal to the broker and does not go through it.
77
+ # Verified on mosquitto 2.0.21 - an anonymous publish is refused while the bridge still forwards.
78
+
79
+ # ============ What gets relayed ============
80
+ #
81
+ # Replace <org> in all four lines with the organization id - the first element of every topic, and
82
+ # the name of the file in config.d/organizations/. One set of four lines per organization on this
83
+ # Pi. To relay only some projects, replace "<org>/" with "<org>/<project>/" and add "+/" where a
84
+ # level is now missing, or repeat the block per project.
85
+ #
86
+ # Frugal IoT topics come in five shapes, and each line below picks out one or two of them. "+"
87
+ # matches exactly one level, so counting levels is what separates them:
88
+ #
89
+ # <org>/<project> the "quick discover" announcement
90
+ # <org>/<project>/<node>/<module>/<field> a reading
91
+ # <org>/<project>/<node>/<module>/<field>/<parameter> a reading's min, max, colour, wiring
92
+ # <org>/<project>/<node>/set/<module>/<field> a control
93
+ # <org>/<project>/<node>/set/<module>/<field>/<parameter>
94
+ #
95
+ # Readings, at five levels. These are the only topics the logger writes to disk, so this is the
96
+ # one line where a topic must not be matched twice - each extra copy would be another row in the
97
+ # CSV files at the far end.
98
+ #
99
+ # The "0" is the QoS, and it is doing real work. At 0, anything published while production is
100
+ # unreachable is simply dropped: production's history has a gap for the outage and this Pi's is
101
+ # complete. At 1 the broker would instead queue it all up and deliver it on reconnect, where
102
+ # production stamps each reading with the time it arrived - so an outage would come back as a
103
+ # whole day's readings all claiming to have happened at the moment the link returned, which is
104
+ # worse than a gap because it looks like real data. Leave this at 0.
105
+ topic <org>/+/+/+/+ out 0
106
+
107
+ # The quick discover announcement, so production learns which nodes live here. Sent periodically
108
+ # and not retained, so nothing accumulates while the link is down.
109
+ topic <org>/+ out 0
110
+
111
+ # Parameters - the min, max, colour and wiring a node announces once at startup. The dashboard
112
+ # needs them to draw a node properly, and the logger never records them.
113
+ topic <org>/+/+/+/+/+ out 0
114
+
115
+ # Controls, in both directions and at QoS 1. Both directions, because a control can be set from
116
+ # either dashboard and both brokers need to know. QoS 1 because here queueing is exactly what is
117
+ # wanted: a control set on production while this Pi is offline is held and delivered when it comes
118
+ # back, which is the same reason nodes publish these retained.
119
+ #
120
+ # At six levels this overlaps the parameter line above, so some controls reach production twice.
121
+ # That is deliberate - it is the harmless place to put the overlap, since a repeated control
122
+ # carries the same value and the logger discards anything with "set" in it. There is no way to
123
+ # write "six levels but not a control", as MQTT patterns cannot exclude.
124
+ topic <org>/+/+/set/# both 1
125
+
126
+ # Loop protection is Mosquitto's "try_private", on by default, which tells production that this is
127
+ # a bridge and not an ordinary client so that it never echoes our own messages back at us. Both
128
+ # ends must be Mosquitto for that to hold - it was measured working between 2.0.21 and itself.
129
+ # TODO revisit if production ever moves to a different broker, or if a third broker is ever
130
+ # bridged into the same topics, as neither case is covered by that protection.
@@ -255,6 +255,13 @@ function send_people_list(req, res) {
255
255
  });
256
256
  }
257
257
 
258
+ // Sessions older than this hold that user's permissions from before the change - see the middleware
259
+ // after passport.authenticate('session')
260
+ const permissionsChangedAt = new Map(); // user id -> when it last changed
261
+ function notePermissionsChanged(id) {
262
+ permissionsChangedAt.set(Number(id), Date.now());
263
+ }
264
+
258
265
  function add_permission(id, capability, org, cb) {
259
266
  if ((id === undefined)
260
267
  || (capability === undefined) || (capability.length < 2)
@@ -267,6 +274,7 @@ function add_permission(id, capability, org, cb) {
267
274
  (cb) => db.get('SELECT COUNT(id) FROM permissions WHERE id = ? AND capability = ? AND org = ?', [id, capability, org], cb),
268
275
  (n_perms, cb) => { if (n_perms["COUNT(id)"] != 0) { cb(new Error("Duplicate permission")); } else { cb(null); }},
269
276
  (cb) => db.run(sqlAddPermission, [id, capability, org], cb),
277
+ (cb) => { notePermissionsChanged(id); cb(null); },
270
278
  ], cb);
271
279
  }
272
280
  }
@@ -278,6 +286,7 @@ function permissions_delete(id, capability, org, cb) {
278
286
  } else {
279
287
  waterfall([
280
288
  (cb) => db.get('DELETE FROM permissions WHERE id = ? AND capability = ? AND org = ?', [id,capability,org], cb),
289
+ (cb) => { notePermissionsChanged(id); cb(null); },
281
290
  ], cb);
282
291
  }
283
292
  }
@@ -806,6 +815,7 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
806
815
  email: user.email,
807
816
  phone: user.phone,
808
817
  permissions: user.permissions,
818
+ loginAt: Date.now(), // So a permission change can tell which sessions predate it
809
819
  });
810
820
  });
811
821
  });
@@ -822,6 +832,30 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
822
832
  // Check if have a session, and if so store in req.user, uses function defined in deserializeUser above
823
833
  app.use(passport.authenticate('session')); // Add user to req.user
824
834
 
835
+ // End any session that predates a change to that user's permissions.
836
+ //
837
+ // The session carries the permissions read at login, and can_READ, can_ADMIN and the rest
838
+ // read them from there - so without this a change takes effect only when the user next logs
839
+ // in. Granting is merely confusing; revoking is worse, because someone whose ADMIN has been
840
+ // taken away keeps it until their session ends.
841
+ //
842
+ // Rather than re-reading the table on every request to catch something this rare, note when
843
+ // a user's permissions changed and end the sessions older than that. They log in again,
844
+ // which is a small price for a change that happens a handful of times.
845
+ //
846
+ // In memory on purpose: express-session's default store keeps the sessions in this process
847
+ // too, so the two are lost together on a restart and neither can outlive the other.
848
+ app.use((req, res, next) => {
849
+ const changedAt = req.user && permissionsChangedAt.get(Number(req.user.id));
850
+ if (changedAt && (!req.user.loginAt || (changedAt > req.user.loginAt))) {
851
+ console.log("Permissions changed for", req.user.id, "- ending the session made before it");
852
+ // Carry on unauthenticated: a page redirects to login, /config.json answers 401 and the
853
+ // client redirects itself
854
+ return req.logout((err) => (err ? next(err) : next()));
855
+ }
856
+ next();
857
+ });
858
+
825
859
  // ===== API Integration: Farm IoT Interoperability Standard =====
826
860
  // Its tables are in frugal-iot-createdb.sql, already run by execSqlStart above, so there is
827
861
  // nothing to initialize here (lib/database.js still exports initializeSchema for the tests).
@@ -864,11 +898,15 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
864
898
  (req,res,next) => {
865
899
  console.log("Trying to login with redirect to",req.body.url);
866
900
  // This is ugly, but I cannot see how to pass the URL to passport.authenticate options
901
+ // Default the destination rather than trusting every caller to supply one: with no
902
+ // successRedirect, passport calls next() on success and the request 404s as
903
+ // "Cannot POST /login" - a confusing way to report a missing form field.
904
+ const returnTo = req.body.url || '/dashboard';
867
905
  passport.authenticate('local', {
868
906
  session: true,
869
907
  //failWithError: true,
870
- failureRedirect: `${loginUrl}?register=false&message=Incorrect+username+or+password&url=${req.body.url}`,
871
- successRedirect: req.body.url,
908
+ failureRedirect: `${loginUrl}?register=false&message=Incorrect+username+or+password&url=${returnTo}`,
909
+ successRedirect: returnTo,
872
910
  // In failure case will also be messages in the session which need clearing out TODO-N89
873
911
  //failureRedirect: `${loginUrl}?register=false&message=Incorrect+username+or+password&url=${req.body.url}`,
874
912
  })(req, res, next);
@@ -1021,6 +1059,16 @@ mqttLogger.readYamlConfig('.', (err, configobj) => {
1021
1059
  },
1022
1060
  send_projects_list,
1023
1061
  );
1062
+ // Log out and go back to the login page. There was no way to do this at all before, which
1063
+ // matters more now that a permissions change ends the session and people log in again.
1064
+ app.get('/logout', (req, res, next) => {
1065
+ // With a url to come back to: without one, POST /login has no successRedirect, so on a
1066
+ // *successful* login passport calls next(), nothing else handles POST /login, and express
1067
+ // answers "Cannot POST /login" - which looks like a broken login rather than a missing
1068
+ // parameter.
1069
+ req.logout((err) => (err ? next(err) : res.redirect(`${loginUrl}?register=false&url=/dashboard`)));
1070
+ });
1071
+
1024
1072
  // /dashboard is served statically, to logged in users //TODO-N89 restrict orgs to those have permissions for (maybe handled via /config.org )
1025
1073
  const routerDashboard = express.Router();
1026
1074
  app.use('/dashboard', routerDashboard);
@@ -19,7 +19,12 @@
19
19
  * appending to them.
20
20
  */
21
21
 
22
- import { createReadStream, createWriteStream, readdir, rename, stat, statfs, unlink, readFile, writeFile } from 'fs';
22
+ import { createReadStream, createWriteStream, readdir, rename, stat, unlink, readFile, writeFile } from 'fs';
23
+ // statfs arrived in node 18.15, and this package says it needs node 18. Taken off the namespace
24
+ // rather than imported by name, because a named import of something a builtin does not have fails
25
+ // when the module loads - which would stop the server outright on an 18.x older than that, instead
26
+ // of just doing without the one check that needs it (see freePercent below).
27
+ import fs from 'fs';
23
28
  import { createGzip, gunzip, gzip } from 'zlib';
24
29
  import path from 'path';
25
30
  import { each, eachSeries, waterfall } from 'async';
@@ -147,8 +152,8 @@ function deleteFiles(files, callback) {
147
152
  * How much of the filesystem holding "dir" is free, as a percentage, or null if it cannot be told.
148
153
  */
149
154
  function freePercent(dir, callback) {
150
- if (typeof statfs !== 'function') { callback(null, null); return; } // Older node
151
- statfs(dir, (err, st) => {
155
+ if (typeof fs.statfs !== 'function') { callback(null, null); return; } // node older than 18.15
156
+ fs.statfs(dir, (err, st) => {
152
157
  if (err || !st || !st.blocks) { callback(null, null); return; }
153
158
  callback(null, (st.bavail / st.blocks) * 100);
154
159
  });
@@ -189,7 +194,8 @@ export function housekeep(datadir, config, callback) {
189
194
  // Delete oldest days until there is enough room. The shortfall is worked out in bytes so
190
195
  // that the number of days removed matches what is actually needed.
191
196
  console.log(`Housekeeping: ${free.toFixed(1)}% of the disk free, below the ${freeWanted}% wanted - removing the oldest readings`);
192
- statfs(datadir, (err3, st) => {
197
+ // Reached only when freePercent returned a number, so statfs exists on this node
198
+ fs.statfs(datadir, (err3, st) => {
193
199
  let shortfall = (err3 || !st) ? 0 : ((freeWanted / 100) * st.blocks - st.bavail) * st.bsize;
194
200
  let toRemove = [];
195
201
  let planned = 0;
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": "^1.3.13",
16
- "frugal-iot-logger": "^1.1.18",
15
+ "frugal-iot-client": "^1.3.14",
16
+ "frugal-iot-logger": "^1.1.20",
17
17
  "hash-wasm": "^4.12.0",
18
18
  "js-yaml": "^4.2.0",
19
19
  "morgan": "^1.11.0",
@@ -27,7 +27,10 @@
27
27
  "frugal-iot-init": "scripts/init.zsh",
28
28
  "frugal-iot-addorganization": "scripts/addorganization.zsh",
29
29
  "frugal-iot-setpassword": "scripts/setpassword.zsh",
30
- "frugal-iot-diagnostic": "scripts/diagnostic.zsh"
30
+ "frugal-iot-diagnostic": "scripts/diagnostic.zsh",
31
+ "frugal-iot-clearretained": "scripts/clearretained.zsh",
32
+ "frugal-iot-addbridge-prod": "scripts/addbridge-prod.zsh",
33
+ "frugal-iot-addbridge-pi": "scripts/addbridge-pi.zsh"
31
34
  },
32
35
  "description": "Frugal IoT client and server",
33
36
  "//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",
@@ -44,6 +47,7 @@
44
47
  "private/",
45
48
  "scripts/",
46
49
  "extras/mosquitto.conf",
50
+ "extras/mosquitto-bridge.conf.example",
47
51
  "extras/frugaliot.service",
48
52
  "API.md",
49
53
  "INSTALLATION.md"
@@ -68,8 +72,9 @@
68
72
  "test:watch": "vitest",
69
73
  "test:coverage": "vitest run --coverage",
70
74
  "check-schema": "node scripts/check-schema.js",
71
- "prerelease": "zsh scripts/prerelease.zsh"
75
+ "prerelease": "zsh scripts/prerelease.zsh",
76
+ "check-files": "node scripts/check-published-files.js"
72
77
  },
73
78
  "type": "module",
74
- "version": "0.3.7"
79
+ "version": "0.3.8"
75
80
  }
package/public/index.html CHANGED
@@ -167,6 +167,14 @@
167
167
  <div class="nav-section">
168
168
  <h2>Other examples</h2>
169
169
  <div class="box-grid">
170
+ <!-- The display the Dashboard used before the cards. Kept reachable while people get used
171
+ to the new one; it will go once nothing needs it. -->
172
+ <div class="nav-box">
173
+ <div class="nav-box-background" style="background-image: url('/images/icon-dashboard.png')"></div>
174
+ <div class="nav-box-content">
175
+ <a href="/dashboard/index-old.html">Previous dashboard</a>
176
+ </div>
177
+ </div>
170
178
  <div class="nav-box">
171
179
  <div class="nav-box-background" style="background-image: url('/images/icon-dashboard.png')"></div>
172
180
  <div class="nav-box-content">
@@ -1,6 +1,7 @@
1
- // Change this version number if want existing installs of PWA to refresh their cache
2
- // TODO-28 - probably make this the same as the app version in frugal-iot-client
3
- const CACHE_NAME = 'frugal-iot-cache-1.3.13';
1
+ // Changing this version is what makes an already installed PWA fetch the app again instead of
2
+ // serving what it cached. It follows the version of frugal-iot-client this release installs, and
3
+ // "npm run prerelease" sets it - so there is normally no reason to edit it by hand.
4
+ const CACHE_NAME = 'frugal-iot-cache-1.3.14';
4
5
  const urlsToCache = [
5
6
  '/',
6
7
  '/index.html',