goosebit 0.1.1__py3-none-any.whl → 0.1.2__py3-none-any.whl

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.
Files changed (52) hide show
  1. goosebit/__init__.py +5 -2
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +59 -39
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +40 -34
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +64 -13
  8. goosebit/api/routes.py +14 -7
  9. goosebit/auth/__init__.py +14 -6
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +110 -10
  12. goosebit/permissions.py +26 -20
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +3 -6
  15. goosebit/settings.py +4 -6
  16. goosebit/telemetry/__init__.py +28 -0
  17. goosebit/telemetry/prometheus.py +10 -0
  18. goosebit/ui/__init__.py +1 -1
  19. goosebit/ui/routes.py +33 -40
  20. goosebit/ui/static/js/devices.js +187 -250
  21. goosebit/ui/static/js/firmware.js +229 -92
  22. goosebit/ui/static/js/index.js +79 -90
  23. goosebit/ui/static/js/logs.js +14 -11
  24. goosebit/ui/static/js/rollouts.js +169 -27
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -51
  27. goosebit/ui/templates/firmware.html +149 -35
  28. goosebit/ui/templates/index.html +9 -26
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +15 -5
  31. goosebit/ui/templates/nav.html +77 -26
  32. goosebit/ui/templates/rollouts.html +62 -39
  33. goosebit/updater/__init__.py +1 -1
  34. goosebit/updater/controller/__init__.py +1 -1
  35. goosebit/updater/controller/v1/__init__.py +1 -1
  36. goosebit/updater/controller/v1/routes.py +53 -35
  37. goosebit/updater/manager.py +205 -103
  38. goosebit/updater/routes.py +4 -7
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/METADATA +53 -3
  42. goosebit-0.1.2.dist-info/RECORD +51 -0
  43. goosebit/updater/download/__init__.py +0 -1
  44. goosebit/updater/download/routes.py +0 -6
  45. goosebit/updater/download/v1/__init__.py +0 -1
  46. goosebit/updater/download/v1/routes.py +0 -13
  47. goosebit/updater/misc.py +0 -57
  48. goosebit/updates/artifacts.py +0 -89
  49. goosebit/updates/version.py +0 -38
  50. goosebit-0.1.1.dist-info/RECORD +0 -53
  51. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
@@ -1,140 +1,277 @@
1
1
  const CHUNK_SIZE = 10 * 1024 * 1024; // 10 MB chunk size
2
- const form = document.getElementById('upload-form');
3
- const fileInput = document.getElementById('file-upload');
4
- const fileSubmit = document.getElementById('file-upload-submit');
5
- const progressBar = document.getElementById('upload-progress');
2
+ const uploadForm = document.getElementById("upload-form");
3
+ const uploadFileInput = document.getElementById("file-upload");
4
+ const uploadFileSubmit = document.getElementById("file-upload-submit");
5
+ const uploadProgressBar = document.getElementById("upload-progress");
6
6
 
7
- form.addEventListener('submit', e => {
7
+ let dataTable;
8
+
9
+ uploadForm.addEventListener("submit", async (e) => {
8
10
  e.preventDefault();
9
- sendFileChunks(fileInput.files[0])
11
+ await sendFileChunks(uploadFileInput.files[0]);
10
12
  });
11
13
 
12
- const sendFileChunks = async (file) => {
14
+ async function sendFileChunks(file) {
13
15
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
14
16
  let start = 0;
15
17
  let uploadedChunks = 0;
16
18
 
17
- fileSubmit.disabled = true
18
- fileInput.disabled = true
19
+ uploadFileSubmit.disabled = true;
20
+ uploadFileSubmit.classList.add("d-none");
21
+ uploadFileInput.disabled = true;
22
+ uploadProgressBar.parentElement.classList.remove("d-none");
19
23
 
20
24
  for (let i = 0; i < totalChunks; i++) {
21
25
  const end = Math.min(start + CHUNK_SIZE, file.size);
22
26
  const chunk = file.slice(start, end);
23
27
  const formData = new FormData();
24
- formData.append('chunk', chunk);
25
- formData.append('filename', file.name);
26
- if (i == 0) {
27
- formData.append('init', true);
28
- } else {
29
- formData.append('init', false);
28
+ formData.append("chunk", chunk);
29
+ formData.append("filename", file.name);
30
+ if (i === 0) {
31
+ formData.append("init", "true");
32
+ } else {
33
+ formData.append("init", "false");
30
34
  }
31
35
 
32
- if (i == totalChunks - 1) {
33
- formData.append('done', true);
36
+ if (i === totalChunks - 1) {
37
+ formData.append("done", "true");
34
38
  } else {
35
- formData.append('done', false);
39
+ formData.append("done", "false");
36
40
  }
37
41
 
38
- const response = await fetch("/ui/upload", {
39
- method: 'POST',
42
+ const response = await fetch("/ui/upload/local", {
43
+ method: "POST",
40
44
  body: formData,
41
45
  });
42
46
 
43
47
  if (response.ok) {
44
48
  uploadedChunks++;
45
49
  const progress = (uploadedChunks / totalChunks) * 100;
46
- progressBar.style.width = `${progress}%`;
47
- progressBar.innerHTML = `${Math.round(progress)}%`;
50
+ uploadProgressBar.style.width = `${progress}%`;
51
+ uploadProgressBar.innerHTML = `${Math.round(progress)}%`;
48
52
  } else {
49
53
  if (response.status === 400) {
50
- result = await response.json()
51
- alerts = document.getElementById("upload-alerts");
54
+ const result = await response.json();
55
+ const alerts = document.getElementById("upload-alerts");
52
56
  alerts.innerHTML = `<div class="alert alert-warning alert-dismissible fade show" role="alert">
53
- ${result["detail"]}
57
+ ${result.detail}
54
58
  <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
55
- </div>`
59
+ </div>`;
56
60
  }
57
61
  }
58
62
 
59
63
  start = end;
60
64
  }
61
65
 
62
- window.setTimeout(function () {
63
- resetProgress()
64
- }, 1000)
65
- };
66
-
67
- function resetProgress() {
68
- fileInput.disabled = false;
69
- fileSubmit.disabled = false;
70
- progressBar.style.width = `0%`;
71
- progressBar.innerHTML = `0%`;
72
- updateFirmwareList();
66
+ window.setTimeout(() => {
67
+ resetProgress();
68
+ }, 1000);
73
69
  }
74
70
 
75
- document.addEventListener("DOMContentLoaded", function() {
76
- updateFirmwareList();
71
+ const urlForm = document.getElementById("url-form");
72
+ const urlFileInput = document.getElementById("file-url");
73
+ const urlFileSubmit = document.getElementById("url-submit");
74
+
75
+ urlForm.addEventListener("submit", async (e) => {
76
+ e.preventDefault();
77
+ await sendFileUrl(urlFileInput.value);
77
78
  });
78
79
 
80
+ async function sendFileUrl(url) {
81
+ const formData = new FormData();
82
+ formData.append("url", url);
79
83
 
80
- function updateFirmwareList() {
81
- const url = '/api/firmware/all';
84
+ const response = await fetch("/ui/upload/remote", {
85
+ method: "POST",
86
+ body: formData,
87
+ });
82
88
 
83
- fetch(url)
84
- .then(response => {
85
- if (!response.ok) {
86
- throw new Error('Request failed');
89
+ if (!response.ok) {
90
+ if (response.status === 400) {
91
+ const result = await response.json();
92
+ const alerts = document.getElementById("url-alerts");
93
+ alerts.innerHTML = `<div class="alert alert-warning alert-dismissible fade show" role="alert">
94
+ ${result.detail}
95
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
96
+ </div>`;
87
97
  }
88
- return response.json();
89
- })
90
- .then(data => {
91
- const list = document.getElementById('firmware-list');
92
- list.innerHTML = "";
93
-
94
- data.forEach(item => {
95
- const listItem = document.createElement('li');
96
- listItem.textContent = `${item["name"]}, size: ${(item["size"] / 1024 / 1024).toFixed(2)} MB`;
97
- listItem.classList = ["list-group-item d-flex justify-content-between align-items-center"];
98
-
99
- const btnGroup = document.createElement("div")
100
- btnGroup.classList = "btn-group"
101
- btnGroup.role = "group"
102
-
103
- const deleteBtn = document.createElement('button');
104
- deleteBtn.innerHTML = "<i class='bi bi-trash'></i>";
105
- deleteBtn.classList = ["btn btn-danger"];
106
- deleteBtn.onclick = function() {deleteFirmware(item["name"])};
107
-
108
- const downloadBtn = document.createElement('button');
109
- downloadBtn.innerHTML = "<i class='bi bi-cloud-download'></i>";
110
- downloadBtn.classList = ["btn btn-primary"];
111
- downloadBtn.onclick = function() {window.location.href = `/api/download/${item["name"]}`};
112
-
113
- btnGroup.appendChild(deleteBtn);
114
- btnGroup.appendChild(downloadBtn);
115
-
116
- listItem.appendChild(btnGroup);
117
- list.appendChild(listItem);
98
+ }
99
+ }
100
+
101
+ function updateFirmwareList() {
102
+ dataTable.ajax.reload(null, false);
103
+ }
104
+
105
+ function resetProgress() {
106
+ uploadFileInput.disabled = false;
107
+ uploadFileSubmit.disabled = false;
108
+ uploadFileSubmit.classList.remove("d-none");
109
+ urlFileInput.disabled = false;
110
+ urlFileSubmit.disabled = false;
111
+ uploadProgressBar.style.width = "0%";
112
+ uploadProgressBar.innerHTML = "0%";
113
+ uploadProgressBar.parentElement.classList.add("d-none");
114
+
115
+ updateFirmwareList();
116
+ }
117
+
118
+ document.addEventListener("DOMContentLoaded", () => {
119
+ dataTable = new DataTable("#firmware-table", {
120
+ responsive: true,
121
+ paging: true,
122
+ processing: false,
123
+ serverSide: true,
124
+ order: [2, "desc"],
125
+ scrollCollapse: true,
126
+ scroller: true,
127
+ scrollY: "60vh",
128
+ stateSave: true,
129
+ stateLoadParams: (settings, data) => {
130
+ // if save state is older than last breaking code change...
131
+ if (data.time <= 1722415428000) {
132
+ // ... delete it
133
+ for (const key of Object.keys(data)) {
134
+ delete data[key];
135
+ }
136
+ }
137
+ },
138
+ ajax: {
139
+ url: "/api/firmware/all",
140
+ contentType: "application/json",
141
+ },
142
+ initComplete: () => {
143
+ updateBtnState();
144
+ },
145
+ columnDefs: [
146
+ {
147
+ targets: "_all",
148
+ searchable: false,
149
+ orderable: false,
150
+ render: (data) => data || "-",
151
+ },
152
+ ],
153
+ columns: [
154
+ { data: "id", visible: false },
155
+ { data: "name", searchable: true },
156
+ { data: "version", searchable: true, orderable: true },
157
+ {
158
+ data: "size",
159
+ render: (data, type) => {
160
+ if (type === "display" || type === "filter") {
161
+ return `${(data / 1024 / 1024).toFixed(2)}MB`;
162
+ }
163
+ return data;
164
+ },
165
+ },
166
+ ],
167
+ select: true,
168
+ rowId: "id",
169
+ layout: {
170
+ bottom1Start: {
171
+ buttons: [
172
+ {
173
+ text: '<i class="bi bi-cloud-download" ></i>',
174
+ action: (e, dt) => {
175
+ const selectedFirmware = dt
176
+ .rows({ selected: true })
177
+ .data()
178
+ .toArray()
179
+ .map((d) => d.id);
180
+ downloadFirmware(selectedFirmware[0]);
181
+ },
182
+ className: "buttons-download",
183
+ titleAttr: "Download Firmware",
184
+ },
185
+ {
186
+ text: '<i class="bi bi-trash" ></i>',
187
+ action: async (e, dt) => {
188
+ const selectedFirmware = dt
189
+ .rows({ selected: true })
190
+ .data()
191
+ .toArray()
192
+ .map((d) => d.id);
193
+ await deleteFirmware(selectedFirmware);
194
+ },
195
+ className: "buttons-delete",
196
+ titleAttr: "Delete Firmware",
197
+ },
198
+ ],
199
+ },
200
+ },
201
+ });
202
+
203
+ dataTable
204
+ .on("select", () => {
205
+ updateBtnState();
206
+ })
207
+ .on("deselect", () => {
208
+ updateBtnState();
118
209
  });
119
- })
120
- .catch(error => {
121
- console.error('Failed to fetch firmware data:', error);
210
+
211
+ // Compatibility tooltip
212
+ $(() => {
213
+ $('[data-toggle="tooltip"]').tooltip();
122
214
  });
215
+
216
+ $("#firmware-table tbody")
217
+ .on("mouseenter", "tr", function () {
218
+ const rowData = dataTable.row(this).data();
219
+ const compat = rowData.compatibility;
220
+ let tooltipText = "";
221
+ if (compat) {
222
+ const result = compat.reduce((acc, { model, revision }) => {
223
+ if (!acc[model]) {
224
+ acc[model] = [];
225
+ }
226
+ acc[model].push(revision);
227
+ return acc;
228
+ }, {});
229
+
230
+ tooltipText = Object.entries(result)
231
+ .map(([model, revision]) => `<b>${model}</b> [${revision.join(", ")}]`)
232
+ .join(", ");
233
+ }
234
+
235
+ // Initialize Bootstrap tooltip
236
+ $(this)
237
+ .attr("title", tooltipText)
238
+ .tooltip({
239
+ placement: "top",
240
+ trigger: "hover",
241
+ container: "body",
242
+ html: true,
243
+ })
244
+ .tooltip("show");
245
+ })
246
+ .on("mouseleave", "tr", function () {
247
+ $(this).tooltip("dispose");
248
+ });
249
+
250
+ updateFirmwareList();
251
+ });
252
+
253
+ function updateBtnState() {
254
+ if (dataTable.rows({ selected: true }).any()) {
255
+ document.querySelector("button.buttons-delete").classList.remove("disabled");
256
+ } else {
257
+ document.querySelector("button.buttons-delete").classList.add("disabled");
258
+ }
259
+ if (dataTable.rows({ selected: true }).count() === 1) {
260
+ document.querySelector("button.buttons-download").classList.remove("disabled");
261
+ } else {
262
+ document.querySelector("button.buttons-download").classList.add("disabled");
263
+ }
123
264
  }
124
265
 
125
- function deleteFirmware(file) {
126
- fetch('/api/firmware/delete', {
127
- method: 'POST',
128
- body: file,
129
- })
130
- .then(response => {
131
- if (!response.ok) {
132
- throw new Error('Failed to delete firmware.');
133
- }
266
+ async function deleteFirmware(files) {
267
+ try {
268
+ await post("/api/firmware/delete", files);
134
269
  updateFirmwareList();
135
- return response.json();
136
- })
137
- .catch(error => {
138
- console.error('Error:', error);
139
- });
270
+ } catch (error) {
271
+ console.error("Deleting firmwares failed:", error);
272
+ }
273
+ }
274
+
275
+ function downloadFirmware(file) {
276
+ window.location.href = `/api/download/${file}`;
140
277
  }
@@ -1,64 +1,80 @@
1
- document.addEventListener("DOMContentLoaded", function() {
2
- var dataTable = new DataTable("#device-table", {
1
+ let dataTable;
2
+
3
+ document.addEventListener("DOMContentLoaded", () => {
4
+ dataTable = new DataTable("#device-table", {
3
5
  responsive: true,
4
- paging: false,
6
+ paging: true,
7
+ processing: false,
8
+ serverSide: true,
5
9
  scrollCollapse: true,
6
10
  scroller: true,
7
11
  scrollY: "65vh",
8
12
  stateSave: true,
13
+ stateLoadParams: (settings, data) => {
14
+ // if save state is older than last breaking code change...
15
+ if (data.time <= 1722434386000) {
16
+ // ... delete it
17
+ for (const key of Object.keys(data)) {
18
+ delete data[key];
19
+ }
20
+ }
21
+ },
9
22
  ajax: {
10
23
  url: "/api/devices/all",
11
- dataSrc: "",
24
+ contentType: "application/json",
12
25
  },
13
- initComplete:function(){
26
+ initComplete: () => {
14
27
  updateBtnState();
15
28
  },
16
29
  columnDefs: [
30
+ {
31
+ targets: [0, 2],
32
+ searchable: true,
33
+ orderable: true,
34
+ },
17
35
  {
18
36
  targets: "_all",
19
- render: function(data, type, row) {
20
- return data || "❓";
21
- },
22
- }
37
+ searchable: false,
38
+ orderable: false,
39
+ render: (data) => data || "-",
40
+ },
23
41
  ],
24
42
  columns: [
25
- { data: 'name' },
43
+ { data: "name" },
26
44
  {
27
- data: 'online',
28
- render: function(data, type, row) {
29
- if ( type === 'display' || type === 'filter' ) {
30
- online = data ? "Online" : "Offline"
31
- color = data ? "success" : "danger"
45
+ data: "online",
46
+ render: (data, type) => {
47
+ if (type === "display" || type === "filter") {
48
+ const color = data ? "success" : "danger";
32
49
  return `
33
50
  <div class="text-${color}">
34
51
 
35
52
  </div>
36
- `
53
+ `;
37
54
  }
38
55
  return data;
39
- }
56
+ },
40
57
  },
41
- { data: 'uuid' },
42
- { data: 'fw' },
58
+ { data: "uuid" },
59
+ { data: "fw_installed_version" },
43
60
  {
44
- data: 'progress',
45
- render: function(data, type, row) {
46
- if ( type === 'display' || type === 'filter' ) {
47
- return (data || "❓") + "%";
61
+ data: "progress",
62
+ render: (data, type) => {
63
+ if (type === "display" || type === "filter") {
64
+ return data ? `${data}%` : "-";
48
65
  }
49
66
  return data;
50
- }
51
-
67
+ },
52
68
  },
53
- { data: 'last_ip' },
69
+ { data: "last_ip" },
54
70
  {
55
- data: 'last_seen',
56
- render: function(data, type, row) {
57
- if ( type === 'display' || type === 'filter' ) {
71
+ data: "last_seen",
72
+ render: (data, type) => {
73
+ if (type === "display" || type === "filter") {
58
74
  return secondsToRecentDate(data);
59
75
  }
60
76
  return data;
61
- }
77
+ },
62
78
  },
63
79
  ],
64
80
  select: true,
@@ -69,103 +85,76 @@ document.addEventListener("DOMContentLoaded", function() {
69
85
  {
70
86
  text: '<i class="bi bi-check-all"></i>',
71
87
  extend: "selectAll",
72
- titleAttr: 'Select All'
88
+ titleAttr: "Select All",
73
89
  },
74
90
  {
75
91
  text: '<i class="bi bi-x"></i>',
76
92
  extend: "selectNone",
77
- titleAttr: 'Clear Selection'
93
+ titleAttr: "Clear Selection",
78
94
  },
79
95
  {
80
96
  text: '<i class="bi bi-file-earmark-arrow-down"></i>',
81
- action: function (e, dt, node, config) {
82
- selectedDevices = dt.rows( {selected:true} ).data().toArray();
97
+ action: (e, dt) => {
98
+ const selectedDevices = dt.rows({ selected: true }).data().toArray();
83
99
  downloadLogins(selectedDevices);
84
100
  },
85
101
  className: "buttons-export-login",
86
- titleAttr: 'Export Login'
102
+ titleAttr: "Export Login",
87
103
  },
88
104
  {
89
105
  text: '<i class="bi bi-file-text"></i>',
90
- action: function (e, dt, node, config) {
91
- selectedDevice = dt.rows( {selected:true} ).data().toArray()[0];
92
- window.location.href = `/ui/logs/${selectedDevice["uuid"]}`;
106
+ action: (e, dt) => {
107
+ const selectedDevice = dt.rows({ selected: true }).data().toArray()[0];
108
+ window.location.href = `/ui/logs/${selectedDevice.uuid}`;
93
109
  },
94
110
  className: "buttons-logs",
95
- titleAttr: 'View Log'
111
+ titleAttr: "View Log",
96
112
  },
97
- ]
98
- }
99
- }
113
+ ],
114
+ },
115
+ },
100
116
  });
101
117
 
102
- dataTable.on( 'select', function ( e, dt, type, indexes ) {
103
- updateBtnState();
104
- } ).on( 'deselect', function ( e, dt, type, indexes ) {
105
- updateBtnState();
106
- } );
118
+ dataTable
119
+ .on("select", () => {
120
+ updateBtnState();
121
+ })
122
+ .on("deselect", () => {
123
+ updateBtnState();
124
+ });
107
125
 
108
- setInterval(function () {
126
+ setInterval(() => {
109
127
  dataTable.ajax.reload(null, false);
110
128
  }, TABLE_UPDATE_TIME);
111
129
  });
112
130
 
113
131
  function updateBtnState() {
114
- dataTable = $("#device-table").DataTable();
115
- if (dataTable.rows( {selected:true} ).any()){
116
- document.querySelector('button.buttons-select-none').classList.remove('disabled');
117
- document.querySelector('button.buttons-export-login').classList.remove('disabled');
132
+ if (dataTable.rows({ selected: true }).any()) {
133
+ document.querySelector("button.buttons-select-none").classList.remove("disabled");
134
+ document.querySelector("button.buttons-export-login").classList.remove("disabled");
118
135
  } else {
119
- document.querySelector('button.buttons-select-none').classList.add('disabled');
120
- document.querySelector('button.buttons-export-login').classList.add('disabled');
136
+ document.querySelector("button.buttons-select-none").classList.add("disabled");
137
+ document.querySelector("button.buttons-export-login").classList.add("disabled");
121
138
  }
122
- if (dataTable.rows( {selected:true} ).count() == 1){
123
- document.querySelector('button.buttons-logs').classList.remove('disabled');
139
+ if (dataTable.rows({ selected: true }).count() === 1) {
140
+ document.querySelector("button.buttons-logs").classList.remove("disabled");
124
141
  } else {
125
- document.querySelector('button.buttons-logs').classList.add('disabled');
126
- }
127
-
128
-
129
- if(dataTable.rows( {selected:true} ).ids().toArray().length === dataTable.rows().ids().toArray().length){
130
- document.querySelector('button.buttons-select-all').classList.add('disabled');
131
- } else {
132
- document.querySelector('button.buttons-select-all').classList.remove('disabled');
142
+ document.querySelector("button.buttons-logs").classList.add("disabled");
133
143
  }
134
144
  }
135
145
 
136
146
  function downloadLogins(devices) {
137
- let deviceLogins = devices.map(dev => {
138
- return [dev["name"], `https://${dev["uuid"]}-access.loadsync.io`, dev["uuid"]];
147
+ const deviceLogins = devices.map((dev) => {
148
+ return [dev.name, `https://${dev.uuid}-access.loadsync.io`, dev.uuid];
139
149
  });
140
150
  deviceLogins.unshift(["Building", "Access Link", "Serial Number/Wifi SSID", "Login/Wifi Password"]);
141
151
 
142
- let csvContent = "data:text/csv;charset=utf-8," + deviceLogins.map(e => e.join(",")).join("\n");
143
- var encodedUri = encodeURI(csvContent);
144
- var link = document.createElement("a");
152
+ const csvContent = `data:text/csv;charset=utf-8,${deviceLogins.map((e) => e.join(",")).join("\n")}`;
153
+ const encodedUri = encodeURI(csvContent);
154
+ const link = document.createElement("a");
145
155
  link.setAttribute("href", encodedUri);
146
156
  link.setAttribute("download", "LoadsyncLogins-Export.csv");
147
157
  document.body.appendChild(link);
148
158
 
149
159
  link.click();
150
160
  }
151
-
152
- function secondsToRecentDate(t) {
153
- if (t == null) {
154
- return null
155
- }
156
- t = Number(t);
157
- var d = Math.floor(t / 86400)
158
- var h = Math.floor(t % 86400 / 3600);
159
- var m = Math.floor(t % 86400 % 3600 / 60);
160
- var s = Math.floor(t % 86400 % 3600 % 60);
161
-
162
- if (d > 0) {
163
- return d + (d == 1 ? " day" : " days");
164
- } else if (h > 0) {
165
- return h + (h == 1 ? " hour" : " hours");
166
- } else if (m > 0) {
167
- return m + (m == 1 ? " minute" : " minutes");
168
- } else {
169
- return s + (s == 1 ? " second" : " seconds");
170
- }
171
- }
@@ -1,22 +1,25 @@
1
- document.addEventListener("DOMContentLoaded", function() {
2
- var logs_ws = create_ws(`/realtime/logs/${device}`);
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const logs_ws = create_ws(`/realtime/logs/${device}`);
3
3
 
4
- logs_ws.addEventListener('message', (event) => {
5
- res = JSON.parse(event.data);
4
+ logs_ws.addEventListener("message", (event) => {
5
+ const res = JSON.parse(event.data);
6
6
 
7
- const logElem = document.getElementById('device-log');
8
- if (res["clear"]) {
9
- logElem.textContent = ""
7
+ const logElem = document.getElementById("device-log");
8
+ if (res.clear) {
9
+ logElem.textContent = "";
10
10
  }
11
- logElem.textContent += res["log"];
11
+ logElem.textContent += res.log;
12
12
 
13
- const progressElem = document.getElementById('install-progress');
13
+ const progressElem = document.getElementById("install-progress");
14
14
  progressElem.style.width = `${res.progress}%`;
15
15
  progressElem.innerHTML = `${res.progress}%`;
16
16
  });
17
17
  });
18
18
 
19
19
  function create_ws(s) {
20
- var l = window.location;
21
- return new WebSocket(((l.protocol === "https:") ? "wss://" : "ws://") + l.hostname + (((l.port != 80) && (l.port != 443)) ? ":" + l.port : "") + s);
20
+ const l = window.location;
21
+ const protocol = l.protocol === "https:" ? "wss://" : "ws://";
22
+ const port = l.port !== "80" || l.port !== "443" ? l.port : "";
23
+ const url = `${protocol}${l.hostname}${port}${s}`;
24
+ return new WebSocket(url);
22
25
  }