--- /dev/null
+#!/usr/bin/env node
+
+/* eslint-env es2020 */
+
+const API_URL = process.env.API_URL;
+const API_KEY = process.env.API_KEY;
+const VERBOSE = !!process.env.VERBOSE;
+
+async function main() {
+ const config = await fetchConfig();
+ const devicesStatus = await fetchDevicesStatus();
+ const connectionsStatus = await fetchConnections();
+
+ for (const {deviceID, name} of config.devices) {
+ const {connected} = connectionsStatus.connections[deviceID];
+ const {lastSeen: lastSeenString} = devicesStatus[deviceID];
+ const lastSeenDate = new Date(lastSeenString);
+ const {completion, needItems} = await fetchCompletion(deviceID);
+ //console.log(name, await fetchCompletion(deviceID));
+
+ checkDevice({name, connected, lastSeenDate, completion, needItems});
+ }
+}
+
+async function checkDevice({name, connected, lastSeenDate, completion, needItems}) {
+ if (VERBOSE) {
+ console.error('checkDevice', {name, connected, lastSeenDate, completion, needItems});
+ }
+
+ if (lastSeenDate.getTime() === 0 || connected) {
+ return;
+ }
+
+ if (lastSeenDate < newDateDays(-10)) {
+ console.log(`Alert! '${name}' has been missing since ` +
+ lastSeenDate.toISOString().replace(/T.*/, ''));
+ }
+ if (needItems > 0 || completion < 100) {
+ console.log(`Alert! '${name}' is out of sync! ` +
+ `${Math.round(completion)}%, missing ${needItems} files.`);
+ }
+}
+
+function newDateDays(days) {
+ return new Date(Date.now() + days * 24 * 3600 * 1000);
+}
+
+async function fetchConfig() {
+ const response = await fetchSyncthing('/rest/config');
+ await assertResponseOk(response);
+ return response.json();
+}
+
+async function fetchDevicesStatus() {
+ const response = await fetchSyncthing('/rest/stats/device');
+ await assertResponseOk(response);
+ return response.json();
+}
+
+async function fetchConnections() {
+ const response = await fetchSyncthing('/rest/system/connections');
+ await assertResponseOk(response);
+ return response.json();
+}
+
+async function fetchCompletion(deviceID) {
+ const response = await fetchSyncthing(`/rest/db/completion?device=${deviceID}`);
+ await assertResponseOk(response);
+ return response.json();
+}
+
+async function fetchSyncthing(path) {
+ return fetch(`${API_URL}${path}`, {
+ headers: {
+ 'X-API-Key': API_KEY,
+ }
+ });
+}
+
+async function assertResponseOk(response) {
+ if (response.ok) {
+ return;
+ }
+ throw new Error(
+ 'Response error! ' +
+ response.status + ' ' + response.statusText +
+ ' -- ' + (await response.text())
+ );
+}
+
+main().catch((err) => {
+ console.error('Exiting due to error!', err);
+ process.exit(1);
+});