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.
- goosebit/__init__.py +8 -5
- goosebit/api/__init__.py +1 -1
- goosebit/api/devices.py +60 -36
- goosebit/api/download.py +28 -14
- goosebit/api/firmware.py +37 -44
- goosebit/api/helper.py +30 -0
- goosebit/api/rollouts.py +87 -0
- goosebit/api/routes.py +15 -7
- goosebit/auth/__init__.py +37 -21
- goosebit/db.py +5 -0
- goosebit/models.py +125 -6
- goosebit/permissions.py +33 -13
- goosebit/realtime/__init__.py +1 -1
- goosebit/realtime/logs.py +4 -6
- goosebit/settings.py +38 -29
- goosebit/telemetry/__init__.py +28 -0
- goosebit/telemetry/prometheus.py +10 -0
- goosebit/ui/__init__.py +1 -1
- goosebit/ui/routes.py +36 -39
- goosebit/ui/static/js/devices.js +191 -239
- goosebit/ui/static/js/firmware.js +234 -88
- goosebit/ui/static/js/index.js +83 -84
- goosebit/ui/static/js/logs.js +17 -10
- goosebit/ui/static/js/rollouts.js +198 -0
- goosebit/ui/static/js/util.js +66 -0
- goosebit/ui/templates/devices.html +75 -42
- goosebit/ui/templates/firmware.html +150 -34
- goosebit/ui/templates/index.html +9 -23
- goosebit/ui/templates/login.html +58 -27
- goosebit/ui/templates/logs.html +18 -3
- goosebit/ui/templates/nav.html +78 -25
- goosebit/ui/templates/rollouts.html +76 -0
- goosebit/updater/__init__.py +1 -1
- goosebit/updater/controller/__init__.py +1 -1
- goosebit/updater/controller/v1/__init__.py +1 -1
- goosebit/updater/controller/v1/routes.py +112 -24
- goosebit/updater/manager.py +237 -94
- goosebit/updater/routes.py +7 -8
- goosebit/updates/__init__.py +70 -0
- goosebit/updates/swdesc.py +83 -0
- goosebit-0.1.2.dist-info/METADATA +123 -0
- goosebit-0.1.2.dist-info/RECORD +51 -0
- goosebit/updater/download/__init__.py +0 -1
- goosebit/updater/download/routes.py +0 -6
- goosebit/updater/download/v1/__init__.py +0 -1
- goosebit/updater/download/v1/routes.py +0 -26
- goosebit/updater/misc.py +0 -69
- goosebit/updater/updates.py +0 -93
- goosebit-0.1.0.dist-info/METADATA +0 -37
- goosebit-0.1.0.dist-info/RECORD +0 -48
- {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
- {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
|
3
|
-
const
|
4
|
-
const
|
5
|
-
const
|
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
|
-
|
7
|
+
let dataTable;
|
8
|
+
|
9
|
+
uploadForm.addEventListener("submit", async (e) => {
|
8
10
|
e.preventDefault();
|
9
|
-
sendFileChunks(
|
11
|
+
await sendFileChunks(uploadFileInput.files[0]);
|
10
12
|
});
|
11
13
|
|
12
|
-
|
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
|
-
|
18
|
-
|
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(
|
25
|
-
formData.append(
|
26
|
-
if (i
|
27
|
-
formData.append(
|
28
|
-
} else
|
29
|
-
formData.append(
|
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
|
33
|
-
formData.append(
|
36
|
+
if (i === totalChunks - 1) {
|
37
|
+
formData.append("done", "true");
|
34
38
|
} else {
|
35
|
-
formData.append(
|
39
|
+
formData.append("done", "false");
|
36
40
|
}
|
37
41
|
|
38
|
-
const response = await fetch("/ui/upload", {
|
39
|
-
method:
|
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
|
-
|
47
|
-
|
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(
|
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.
|
67
|
-
|
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
|
-
|
72
|
-
|
84
|
+
const response = await fetch("/ui/upload/remote", {
|
85
|
+
method: "POST",
|
86
|
+
body: formData,
|
87
|
+
});
|
73
88
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
112
|
-
|
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(
|
117
|
-
|
118
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
}
|
goosebit/ui/static/js/index.js
CHANGED
@@ -1,54 +1,80 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
let dataTable;
|
2
|
+
|
3
|
+
document.addEventListener("DOMContentLoaded", () => {
|
4
|
+
dataTable = new DataTable("#device-table", {
|
3
5
|
responsive: true,
|
4
|
-
paging:
|
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
|
-
|
24
|
+
contentType: "application/json",
|
12
25
|
},
|
13
|
-
initComplete:
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
}
|
37
|
+
searchable: false,
|
38
|
+
orderable: false,
|
39
|
+
render: (data) => data || "-",
|
40
|
+
},
|
23
41
|
],
|
24
42
|
columns: [
|
25
|
-
{ data:
|
43
|
+
{ data: "name" },
|
26
44
|
{
|
27
|
-
data:
|
28
|
-
render:
|
29
|
-
if (
|
30
|
-
|
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:
|
42
|
-
{ data: 'fw' },
|
43
|
-
{ data: 'last_ip' },
|
69
|
+
{ data: "last_ip" },
|
44
70
|
{
|
45
|
-
data:
|
46
|
-
render:
|
47
|
-
if (
|
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:
|
88
|
+
titleAttr: "Select All",
|
63
89
|
},
|
64
90
|
{
|
65
91
|
text: '<i class="bi bi-x"></i>',
|
66
92
|
extend: "selectNone",
|
67
|
-
titleAttr:
|
93
|
+
titleAttr: "Clear Selection",
|
68
94
|
},
|
69
95
|
{
|
70
96
|
text: '<i class="bi bi-file-earmark-arrow-down"></i>',
|
71
|
-
action:
|
72
|
-
selectedDevices = dt.rows(
|
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:
|
102
|
+
titleAttr: "Export Login",
|
77
103
|
},
|
78
104
|
{
|
79
105
|
text: '<i class="bi bi-file-text"></i>',
|
80
|
-
action:
|
81
|
-
selectedDevice = dt.rows(
|
82
|
-
window.location.href = `/ui/logs/${selectedDevice
|
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:
|
111
|
+
titleAttr: "View Log",
|
86
112
|
},
|
87
|
-
]
|
88
|
-
}
|
89
|
-
}
|
113
|
+
],
|
114
|
+
},
|
115
|
+
},
|
90
116
|
});
|
91
117
|
|
92
|
-
dataTable
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
118
|
+
dataTable
|
119
|
+
.on("select", () => {
|
120
|
+
updateBtnState();
|
121
|
+
})
|
122
|
+
.on("deselect", () => {
|
123
|
+
updateBtnState();
|
124
|
+
});
|
97
125
|
|
98
|
-
setInterval(
|
126
|
+
setInterval(() => {
|
99
127
|
dataTable.ajax.reload(null, false);
|
100
128
|
}, TABLE_UPDATE_TIME);
|
101
129
|
});
|
102
130
|
|
103
131
|
function updateBtnState() {
|
104
|
-
dataTable
|
105
|
-
|
106
|
-
document.querySelector(
|
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(
|
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(
|
142
|
+
document.querySelector("button.buttons-logs").classList.add("disabled");
|
123
143
|
}
|
124
144
|
}
|
125
145
|
|
126
146
|
function downloadLogins(devices) {
|
127
|
-
|
128
|
-
return [dev
|
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
|
-
|
133
|
-
|
134
|
-
|
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
|
-
}
|
goosebit/ui/static/js/logs.js
CHANGED
@@ -1,18 +1,25 @@
|
|
1
|
-
document.addEventListener("DOMContentLoaded",
|
2
|
-
|
1
|
+
document.addEventListener("DOMContentLoaded", () => {
|
2
|
+
const logs_ws = create_ws(`/realtime/logs/${device}`);
|
3
3
|
|
4
|
-
logs_ws.addEventListener(
|
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(
|
8
|
-
if (res
|
9
|
-
logElem.textContent = ""
|
7
|
+
const logElem = document.getElementById("device-log");
|
8
|
+
if (res.clear) {
|
9
|
+
logElem.textContent = "";
|
10
10
|
}
|
11
|
-
logElem.textContent += res
|
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
|
-
|
17
|
-
|
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
|
}
|