goosebit 0.1.0__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 +8 -5
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +60 -36
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +37 -44
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +87 -0
  8. goosebit/api/routes.py +15 -7
  9. goosebit/auth/__init__.py +37 -21
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +125 -6
  12. goosebit/permissions.py +33 -13
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +4 -6
  15. goosebit/settings.py +38 -29
  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 +36 -39
  20. goosebit/ui/static/js/devices.js +191 -239
  21. goosebit/ui/static/js/firmware.js +234 -88
  22. goosebit/ui/static/js/index.js +83 -84
  23. goosebit/ui/static/js/logs.js +17 -10
  24. goosebit/ui/static/js/rollouts.js +198 -0
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -42
  27. goosebit/ui/templates/firmware.html +150 -34
  28. goosebit/ui/templates/index.html +9 -23
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +18 -3
  31. goosebit/ui/templates/nav.html +78 -25
  32. goosebit/ui/templates/rollouts.html +76 -0
  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 +112 -24
  37. goosebit/updater/manager.py +237 -94
  38. goosebit/updater/routes.py +7 -8
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. goosebit-0.1.2.dist-info/METADATA +123 -0
  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 -26
  47. goosebit/updater/misc.py +0 -69
  48. goosebit/updater/updates.py +0 -93
  49. goosebit-0.1.0.dist-info/METADATA +0 -37
  50. goosebit-0.1.0.dist-info/RECORD +0 -48
  51. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
@@ -1,131 +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)}%`;
52
+ } else {
53
+ if (response.status === 400) {
54
+ const result = await response.json();
55
+ const alerts = document.getElementById("upload-alerts");
56
+ alerts.innerHTML = `<div class="alert alert-warning alert-dismissible fade show" role="alert">
57
+ ${result.detail}
58
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
59
+ </div>`;
60
+ }
48
61
  }
49
62
 
50
63
  start = end;
51
64
  }
52
65
 
53
- window.setTimeout(function () {
54
- resetProgress()
55
- }, 1000)
56
- };
57
-
58
- function resetProgress() {
59
- fileInput.disabled = false;
60
- fileSubmit.disabled = false;
61
- progressBar.style.width = `0%`;
62
- progressBar.innerHTML = `0%`;
63
- updateFirmwareList();
66
+ window.setTimeout(() => {
67
+ resetProgress();
68
+ }, 1000);
64
69
  }
65
70
 
66
- document.addEventListener("DOMContentLoaded", function() {
67
- 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);
68
78
  });
69
79
 
80
+ async function sendFileUrl(url) {
81
+ const formData = new FormData();
82
+ formData.append("url", url);
70
83
 
71
- function updateFirmwareList() {
72
- const url = '/api/firmware/all';
84
+ const response = await fetch("/ui/upload/remote", {
85
+ method: "POST",
86
+ body: formData,
87
+ });
73
88
 
74
- fetch(url)
75
- .then(response => {
76
- if (!response.ok) {
77
- 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>`;
78
97
  }
79
- return response.json();
80
- })
81
- .then(data => {
82
- const list = document.getElementById('firmware-list');
83
- list.innerHTML = "";
84
-
85
- data.forEach(item => {
86
- const listItem = document.createElement('li');
87
- listItem.textContent = item["version"];
88
- listItem.classList = ["list-group-item d-flex justify-content-between align-items-center"];
89
-
90
- const btnGroup = document.createElement("div")
91
- btnGroup.classList = "btn-group"
92
- btnGroup.role = "group"
93
-
94
- const deleteBtn = document.createElement('button');
95
- deleteBtn.innerHTML = "<i class='bi bi-trash'></i>";
96
- deleteBtn.classList = ["btn btn-danger"];
97
- deleteBtn.onclick = function() {deleteFirmware(item["name"])};
98
-
99
- const downloadBtn = document.createElement('button');
100
- downloadBtn.innerHTML = "<i class='bi bi-cloud-download'></i>";
101
- downloadBtn.classList = ["btn btn-primary"];
102
- downloadBtn.onclick = function() {window.location.href = `/api/download/${item["name"]}`};
103
-
104
- btnGroup.appendChild(deleteBtn);
105
- btnGroup.appendChild(downloadBtn);
106
-
107
- listItem.appendChild(btnGroup);
108
- 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();
109
209
  });
110
- })
111
- .catch(error => {
112
- console.error('Failed to fetch firmware data:', error);
210
+
211
+ // Compatibility tooltip
212
+ $(() => {
213
+ $('[data-toggle="tooltip"]').tooltip();
113
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
+ }
114
264
  }
115
265
 
116
- function deleteFirmware(file) {
117
- fetch('/api/firmware/delete', {
118
- method: 'POST',
119
- body: file,
120
- })
121
- .then(response => {
122
- if (!response.ok) {
123
- throw new Error('Failed to delete firmware.');
124
- }
266
+ async function deleteFirmware(files) {
267
+ try {
268
+ await post("/api/firmware/delete", files);
125
269
  updateFirmwareList();
126
- return response.json();
127
- })
128
- .catch(error => {
129
- console.error('Error:', error);
130
- });
270
+ } catch (error) {
271
+ console.error("Deleting firmwares failed:", error);
272
+ }
273
+ }
274
+
275
+ function downloadFirmware(file) {
276
+ window.location.href = `/api/download/${file}`;
131
277
  }
@@ -1,54 +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
+ },
57
+ },
58
+ { data: "uuid" },
59
+ { data: "fw_installed_version" },
60
+ {
61
+ data: "progress",
62
+ render: (data, type) => {
63
+ if (type === "display" || type === "filter") {
64
+ return data ? `${data}%` : "-";
65
+ }
66
+ return data;
67
+ },
40
68
  },
41
- { data: 'uuid' },
42
- { data: 'fw' },
43
- { data: 'last_ip' },
69
+ { data: "last_ip" },
44
70
  {
45
- data: 'last_seen',
46
- render: function(data, type, row) {
47
- if ( type === 'display' || type === 'filter' ) {
71
+ data: "last_seen",
72
+ render: (data, type) => {
73
+ if (type === "display" || type === "filter") {
48
74
  return secondsToRecentDate(data);
49
75
  }
50
76
  return data;
51
- }
77
+ },
52
78
  },
53
79
  ],
54
80
  select: true,
@@ -59,103 +85,76 @@ document.addEventListener("DOMContentLoaded", function() {
59
85
  {
60
86
  text: '<i class="bi bi-check-all"></i>',
61
87
  extend: "selectAll",
62
- titleAttr: 'Select All'
88
+ titleAttr: "Select All",
63
89
  },
64
90
  {
65
91
  text: '<i class="bi bi-x"></i>',
66
92
  extend: "selectNone",
67
- titleAttr: 'Clear Selection'
93
+ titleAttr: "Clear Selection",
68
94
  },
69
95
  {
70
96
  text: '<i class="bi bi-file-earmark-arrow-down"></i>',
71
- action: function (e, dt, node, config) {
72
- selectedDevices = dt.rows( {selected:true} ).data().toArray();
97
+ action: (e, dt) => {
98
+ const selectedDevices = dt.rows({ selected: true }).data().toArray();
73
99
  downloadLogins(selectedDevices);
74
100
  },
75
101
  className: "buttons-export-login",
76
- titleAttr: 'Export Login'
102
+ titleAttr: "Export Login",
77
103
  },
78
104
  {
79
105
  text: '<i class="bi bi-file-text"></i>',
80
- action: function (e, dt, node, config) {
81
- selectedDevice = dt.rows( {selected:true} ).data().toArray()[0];
82
- 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}`;
83
109
  },
84
110
  className: "buttons-logs",
85
- titleAttr: 'View Log'
111
+ titleAttr: "View Log",
86
112
  },
87
- ]
88
- }
89
- }
113
+ ],
114
+ },
115
+ },
90
116
  });
91
117
 
92
- dataTable.on( 'select', function ( e, dt, type, indexes ) {
93
- updateBtnState();
94
- } ).on( 'deselect', function ( e, dt, type, indexes ) {
95
- updateBtnState();
96
- } );
118
+ dataTable
119
+ .on("select", () => {
120
+ updateBtnState();
121
+ })
122
+ .on("deselect", () => {
123
+ updateBtnState();
124
+ });
97
125
 
98
- setInterval(function () {
126
+ setInterval(() => {
99
127
  dataTable.ajax.reload(null, false);
100
128
  }, TABLE_UPDATE_TIME);
101
129
  });
102
130
 
103
131
  function updateBtnState() {
104
- dataTable = $("#device-table").DataTable();
105
- if (dataTable.rows( {selected:true} ).any()){
106
- document.querySelector('button.buttons-select-none').classList.remove('disabled');
107
- document.querySelector('button.buttons-export-login').classList.remove('disabled');
108
- } else {
109
- document.querySelector('button.buttons-select-none').classList.add('disabled');
110
- document.querySelector('button.buttons-export-login').classList.add('disabled');
111
- }
112
- if (dataTable.rows( {selected:true} ).count() == 1){
113
- document.querySelector('button.buttons-logs').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");
114
135
  } else {
115
- document.querySelector('button.buttons-logs').classList.add('disabled');
136
+ document.querySelector("button.buttons-select-none").classList.add("disabled");
137
+ document.querySelector("button.buttons-export-login").classList.add("disabled");
116
138
  }
117
-
118
-
119
- if(dataTable.rows( {selected:true} ).ids().toArray().length === dataTable.rows().ids().toArray().length){
120
- document.querySelector('button.buttons-select-all').classList.add('disabled');
139
+ if (dataTable.rows({ selected: true }).count() === 1) {
140
+ document.querySelector("button.buttons-logs").classList.remove("disabled");
121
141
  } else {
122
- document.querySelector('button.buttons-select-all').classList.remove('disabled');
142
+ document.querySelector("button.buttons-logs").classList.add("disabled");
123
143
  }
124
144
  }
125
145
 
126
146
  function downloadLogins(devices) {
127
- let deviceLogins = devices.map(dev => {
128
- return [dev["name"], `https://${dev["uuid"]}-access.loadsync.io`, dev["uuid"], dev["web_pwd"]];
147
+ const deviceLogins = devices.map((dev) => {
148
+ return [dev.name, `https://${dev.uuid}-access.loadsync.io`, dev.uuid];
129
149
  });
130
150
  deviceLogins.unshift(["Building", "Access Link", "Serial Number/Wifi SSID", "Login/Wifi Password"]);
131
151
 
132
- let csvContent = "data:text/csv;charset=utf-8," + deviceLogins.map(e => e.join(",")).join("\n");
133
- var encodedUri = encodeURI(csvContent);
134
- 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");
135
155
  link.setAttribute("href", encodedUri);
136
156
  link.setAttribute("download", "LoadsyncLogins-Export.csv");
137
157
  document.body.appendChild(link);
138
158
 
139
159
  link.click();
140
160
  }
141
-
142
- function secondsToRecentDate(t) {
143
- if (t == null) {
144
- return null
145
- }
146
- t = Number(t);
147
- var d = Math.floor(t / 86400)
148
- var h = Math.floor(t % 86400 / 3600);
149
- var m = Math.floor(t % 86400 % 3600 / 60);
150
- var s = Math.floor(t % 86400 % 3600 % 60);
151
-
152
- if (d > 0) {
153
- return d + (d == 1 ? " day" : " days");
154
- } else if (h > 0) {
155
- return h + (h == 1 ? " hour" : " hours");
156
- } else if (m > 0) {
157
- return m + (m == 1 ? " minute" : " minutes");
158
- } else {
159
- return s + (s == 1 ? " second" : " seconds");
160
- }
161
- }
@@ -1,18 +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
+
13
+ const progressElem = document.getElementById("install-progress");
14
+ progressElem.style.width = `${res.progress}%`;
15
+ progressElem.innerHTML = `${res.progress}%`;
12
16
  });
13
17
  });
14
18
 
15
19
  function create_ws(s) {
16
- var l = window.location;
17
- 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);
18
25
  }