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.
- goosebit/__init__.py +5 -2
- goosebit/api/__init__.py +1 -1
- goosebit/api/devices.py +59 -39
- goosebit/api/download.py +28 -14
- goosebit/api/firmware.py +40 -34
- goosebit/api/helper.py +30 -0
- goosebit/api/rollouts.py +64 -13
- goosebit/api/routes.py +14 -7
- goosebit/auth/__init__.py +14 -6
- goosebit/db.py +5 -0
- goosebit/models.py +110 -10
- goosebit/permissions.py +26 -20
- goosebit/realtime/__init__.py +1 -1
- goosebit/realtime/logs.py +3 -6
- goosebit/settings.py +4 -6
- goosebit/telemetry/__init__.py +28 -0
- goosebit/telemetry/prometheus.py +10 -0
- goosebit/ui/__init__.py +1 -1
- goosebit/ui/routes.py +33 -40
- goosebit/ui/static/js/devices.js +187 -250
- goosebit/ui/static/js/firmware.js +229 -92
- goosebit/ui/static/js/index.js +79 -90
- goosebit/ui/static/js/logs.js +14 -11
- goosebit/ui/static/js/rollouts.js +169 -27
- goosebit/ui/static/js/util.js +66 -0
- goosebit/ui/templates/devices.html +75 -51
- goosebit/ui/templates/firmware.html +149 -35
- goosebit/ui/templates/index.html +9 -26
- goosebit/ui/templates/login.html +58 -27
- goosebit/ui/templates/logs.html +15 -5
- goosebit/ui/templates/nav.html +77 -26
- goosebit/ui/templates/rollouts.html +62 -39
- 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 +53 -35
- goosebit/updater/manager.py +205 -103
- goosebit/updater/routes.py +4 -7
- goosebit/updates/__init__.py +70 -0
- goosebit/updates/swdesc.py +83 -0
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/METADATA +53 -3
- 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 -13
- goosebit/updater/misc.py +0 -57
- goosebit/updates/artifacts.py +0 -89
- goosebit/updates/version.py +0 -38
- goosebit-0.1.1.dist-info/RECORD +0 -53
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
goosebit/ui/static/js/devices.js
CHANGED
@@ -1,84 +1,105 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
let dataTable;
|
2
|
+
|
3
|
+
document.addEventListener("DOMContentLoaded", async () => {
|
4
|
+
dataTable = new DataTable("#device-table", {
|
3
5
|
responsive: true,
|
4
|
-
paging:
|
6
|
+
paging: true,
|
7
|
+
processing: false,
|
8
|
+
serverSide: true,
|
9
|
+
order: [],
|
5
10
|
scrollCollapse: true,
|
6
11
|
scroller: true,
|
7
12
|
scrollY: "65vh",
|
8
13
|
stateSave: true,
|
14
|
+
stateLoadParams: (settings, data) => {
|
15
|
+
// if save state is older than last breaking code change...
|
16
|
+
if (data.time <= 1722434189000) {
|
17
|
+
// ... delete it
|
18
|
+
for (const key of Object.keys(data)) {
|
19
|
+
delete data[key];
|
20
|
+
}
|
21
|
+
}
|
22
|
+
},
|
9
23
|
select: true,
|
10
24
|
rowId: "uuid",
|
11
25
|
ajax: {
|
12
26
|
url: "/api/devices/all",
|
13
|
-
|
27
|
+
contentType: "application/json",
|
14
28
|
},
|
15
|
-
initComplete:
|
29
|
+
initComplete: () => {
|
16
30
|
updateBtnState();
|
17
31
|
},
|
18
32
|
columnDefs: [
|
33
|
+
{
|
34
|
+
targets: [1, 2, 3, 4, 5, 6, 9, 10],
|
35
|
+
searchable: true,
|
36
|
+
orderable: true,
|
37
|
+
},
|
19
38
|
{
|
20
39
|
targets: "_all",
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
}
|
40
|
+
searchable: false,
|
41
|
+
orderable: false,
|
42
|
+
render: (data) => data || "-",
|
43
|
+
},
|
25
44
|
],
|
26
45
|
columns: [
|
27
|
-
{ data: 'name' },
|
28
46
|
{
|
29
|
-
data:
|
30
|
-
render:
|
31
|
-
if (
|
32
|
-
color = data ? "success" : "danger"
|
47
|
+
data: "online",
|
48
|
+
render: (data, type) => {
|
49
|
+
if (type === "display" || type === "filter") {
|
50
|
+
const color = data ? "success" : "danger";
|
33
51
|
return `
|
34
52
|
<div class="text-${color}">
|
35
53
|
●
|
36
54
|
</div>
|
37
|
-
|
55
|
+
`;
|
38
56
|
}
|
39
57
|
return data;
|
40
|
-
}
|
58
|
+
},
|
41
59
|
},
|
42
|
-
{ data:
|
43
|
-
{ data:
|
44
|
-
{ data:
|
45
|
-
{ data:
|
60
|
+
{ data: "uuid" },
|
61
|
+
{ data: "name" },
|
62
|
+
{ data: "hw_model" },
|
63
|
+
{ data: "hw_revision" },
|
64
|
+
{ data: "feed" },
|
65
|
+
{ data: "flavor" },
|
66
|
+
{ data: "fw_installed_version" },
|
67
|
+
{ data: "fw_target_version" },
|
68
|
+
{ data: "update_mode" },
|
69
|
+
{ data: "state" },
|
46
70
|
{
|
47
|
-
data:
|
48
|
-
render:
|
49
|
-
if (
|
50
|
-
color = data ? "success" : "
|
71
|
+
data: "force_update",
|
72
|
+
render: (data, type) => {
|
73
|
+
if (type === "display" || type === "filter") {
|
74
|
+
const color = data ? "success" : "muted";
|
51
75
|
return `
|
52
76
|
<div class="text-${color}">
|
53
77
|
●
|
54
78
|
</div>
|
55
|
-
|
79
|
+
`;
|
56
80
|
}
|
57
81
|
return data;
|
58
|
-
}
|
82
|
+
},
|
59
83
|
},
|
60
|
-
{ data: 'fw_file' },
|
61
84
|
{
|
62
|
-
data:
|
63
|
-
render:
|
64
|
-
if (
|
65
|
-
return
|
85
|
+
data: "progress",
|
86
|
+
render: (data, type) => {
|
87
|
+
if (type === "display" || type === "filter") {
|
88
|
+
return data ? `${data}%` : "-";
|
66
89
|
}
|
67
90
|
return data;
|
68
|
-
}
|
69
|
-
|
91
|
+
},
|
70
92
|
},
|
71
|
-
{ data:
|
93
|
+
{ data: "last_ip" },
|
72
94
|
{
|
73
|
-
data:
|
74
|
-
render:
|
75
|
-
if (
|
95
|
+
data: "last_seen",
|
96
|
+
render: (data, type) => {
|
97
|
+
if (type === "display" || type === "filter") {
|
76
98
|
return secondsToRecentDate(data);
|
77
99
|
}
|
78
100
|
return data;
|
79
|
-
}
|
101
|
+
},
|
80
102
|
},
|
81
|
-
{ data: 'state' },
|
82
103
|
],
|
83
104
|
layout: {
|
84
105
|
top1Start: {
|
@@ -86,275 +107,212 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
86
107
|
{
|
87
108
|
text: '<i class="bi bi-check-all"></i>',
|
88
109
|
extend: "selectAll",
|
89
|
-
titleAttr:
|
110
|
+
titleAttr: "Select All",
|
90
111
|
},
|
91
112
|
{
|
92
113
|
text: '<i class="bi bi-x"></i>',
|
93
114
|
extend: "selectNone",
|
94
|
-
titleAttr:
|
115
|
+
titleAttr: "Clear Selection",
|
95
116
|
},
|
96
117
|
{
|
97
118
|
text: '<i class="bi bi-file-text"></i>',
|
98
|
-
action:
|
99
|
-
selectedDevice = dataTable.rows(
|
100
|
-
window.location.href = `/ui/logs/${selectedDevice
|
119
|
+
action: () => {
|
120
|
+
const selectedDevice = dataTable.rows({ selected: true }).data().toArray()[0];
|
121
|
+
window.location.href = `/ui/logs/${selectedDevice.uuid}`;
|
101
122
|
},
|
102
123
|
className: "buttons-logs",
|
103
|
-
titleAttr:
|
124
|
+
titleAttr: "View Log",
|
104
125
|
},
|
105
|
-
]
|
126
|
+
],
|
106
127
|
},
|
107
128
|
bottom1Start: {
|
108
129
|
buttons: [
|
109
130
|
{
|
110
131
|
text: '<i class="bi bi-pen" ></i>',
|
111
|
-
action:
|
132
|
+
action: (e, dt) => {
|
112
133
|
const input = document.getElementById("device-selected-name");
|
113
|
-
|
114
|
-
|
134
|
+
input.value = dt
|
135
|
+
.rows({ selected: true })
|
136
|
+
.data()
|
137
|
+
.toArray()
|
138
|
+
.map((d) => d.name)[0];
|
115
139
|
|
116
|
-
new bootstrap.Modal(
|
140
|
+
new bootstrap.Modal("#device-rename-modal").show();
|
117
141
|
},
|
118
142
|
className: "buttons-rename",
|
119
|
-
titleAttr:
|
143
|
+
titleAttr: "Rename Devices",
|
120
144
|
},
|
121
145
|
{
|
122
146
|
text: '<i class="bi bi-gear" ></i>',
|
123
|
-
action:
|
124
|
-
|
147
|
+
action: () => {
|
148
|
+
const selectedDevice = dataTable.rows({ selected: true }).data().toArray()[0];
|
149
|
+
$("#rollout-selected-feed").val(selectedDevice.feed);
|
150
|
+
$("#rollout-selected-flavor").val(selectedDevice.flavor);
|
151
|
+
|
152
|
+
let selectedValue;
|
153
|
+
if (selectedDevice.update_mode === "Rollout") {
|
154
|
+
selectedValue = "rollout";
|
155
|
+
} else if (selectedDevice.update_mode === "Latest") {
|
156
|
+
selectedValue = "latest";
|
157
|
+
} else {
|
158
|
+
selectedValue = selectedDevice.fw_assigned;
|
159
|
+
}
|
160
|
+
$("#selected-fw").val(selectedValue);
|
161
|
+
|
162
|
+
new bootstrap.Modal("#device-config-modal").show();
|
125
163
|
},
|
126
164
|
className: "buttons-config",
|
127
|
-
titleAttr:
|
165
|
+
titleAttr: "Configure Devices",
|
128
166
|
},
|
129
167
|
{
|
130
168
|
text: '<i class="bi bi-trash" ></i>',
|
131
|
-
action:
|
132
|
-
selectedDevices = dt
|
169
|
+
action: (e, dt) => {
|
170
|
+
const selectedDevices = dt
|
171
|
+
.rows({ selected: true })
|
172
|
+
.data()
|
173
|
+
.toArray()
|
174
|
+
.map((d) => d.uuid);
|
133
175
|
deleteDevices(selectedDevices);
|
134
176
|
},
|
135
177
|
className: "buttons-delete",
|
136
|
-
titleAttr:
|
178
|
+
titleAttr: "Delete Devices",
|
137
179
|
},
|
138
180
|
{
|
139
181
|
text: '<i class="bi bi-box-arrow-in-up-right"></i>',
|
140
|
-
action:
|
141
|
-
selectedDevices = dataTable
|
182
|
+
action: () => {
|
183
|
+
const selectedDevices = dataTable
|
184
|
+
.rows({ selected: true })
|
185
|
+
.data()
|
186
|
+
.toArray()
|
187
|
+
.map((d) => d.uuid);
|
142
188
|
forceUpdateDevices(selectedDevices);
|
143
189
|
},
|
144
190
|
className: "buttons-force-update",
|
145
|
-
titleAttr:
|
191
|
+
titleAttr: "Force Update",
|
146
192
|
},
|
147
193
|
{
|
148
194
|
text: '<i class="bi bi-pin-angle"></i>',
|
149
|
-
action:
|
150
|
-
selectedDevices = dataTable
|
195
|
+
action: () => {
|
196
|
+
const selectedDevices = dataTable
|
197
|
+
.rows({ selected: true })
|
198
|
+
.data()
|
199
|
+
.toArray()
|
200
|
+
.map((d) => d.uuid);
|
151
201
|
pinDevices(selectedDevices);
|
152
202
|
},
|
153
203
|
className: "buttons-pin",
|
154
|
-
titleAttr:
|
204
|
+
titleAttr: "Pin Version",
|
155
205
|
},
|
156
|
-
]
|
157
|
-
}
|
206
|
+
],
|
207
|
+
},
|
158
208
|
},
|
159
209
|
});
|
160
210
|
|
161
|
-
dataTable.on(
|
162
|
-
|
163
|
-
|
164
|
-
updateDeviceName(uuid);
|
211
|
+
dataTable.on("click", "button.edit-name", (e) => {
|
212
|
+
const data = dataTable.row(e.target.closest("tr")).data();
|
213
|
+
updateDeviceName(data.uuid);
|
165
214
|
});
|
166
215
|
|
167
|
-
dataTable
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
216
|
+
dataTable
|
217
|
+
.on("select", () => {
|
218
|
+
updateBtnState();
|
219
|
+
})
|
220
|
+
.on("deselect", () => {
|
221
|
+
updateBtnState();
|
222
|
+
});
|
172
223
|
|
173
|
-
setInterval(
|
224
|
+
setInterval(() => {
|
174
225
|
dataTable.ajax.reload(null, false);
|
175
226
|
}, TABLE_UPDATE_TIME);
|
176
227
|
|
177
|
-
updateFirmwareSelection();
|
228
|
+
await updateFirmwareSelection(true);
|
178
229
|
});
|
179
230
|
|
180
|
-
|
181
231
|
function updateBtnState() {
|
182
|
-
dataTable
|
183
|
-
|
184
|
-
document.querySelector(
|
185
|
-
document.querySelector(
|
186
|
-
document.querySelector(
|
187
|
-
document.querySelector(
|
188
|
-
document.querySelector('button.buttons-pin').classList.remove('disabled');
|
189
|
-
} else {
|
190
|
-
document.querySelector('button.buttons-select-none').classList.add('disabled');
|
191
|
-
document.querySelector('button.buttons-config').classList.add('disabled');
|
192
|
-
document.querySelector('button.buttons-force-update').classList.add('disabled');
|
193
|
-
document.querySelector('button.buttons-delete').classList.add('disabled');
|
194
|
-
document.querySelector('button.buttons-pin').classList.add('disabled');
|
195
|
-
}
|
196
|
-
if (dataTable.rows( {selected:true} ).count() == 1){
|
197
|
-
document.querySelector('button.buttons-logs').classList.remove('disabled');
|
198
|
-
document.querySelector('button.buttons-rename').classList.remove('disabled');
|
232
|
+
if (dataTable.rows({ selected: true }).any()) {
|
233
|
+
document.querySelector("button.buttons-select-none").classList.remove("disabled");
|
234
|
+
document.querySelector("button.buttons-config").classList.remove("disabled");
|
235
|
+
document.querySelector("button.buttons-force-update").classList.remove("disabled");
|
236
|
+
document.querySelector("button.buttons-delete").classList.remove("disabled");
|
237
|
+
document.querySelector("button.buttons-pin").classList.remove("disabled");
|
199
238
|
} else {
|
200
|
-
document.querySelector(
|
201
|
-
document.querySelector(
|
239
|
+
document.querySelector("button.buttons-select-none").classList.add("disabled");
|
240
|
+
document.querySelector("button.buttons-config").classList.add("disabled");
|
241
|
+
document.querySelector("button.buttons-force-update").classList.add("disabled");
|
242
|
+
document.querySelector("button.buttons-delete").classList.add("disabled");
|
243
|
+
document.querySelector("button.buttons-pin").classList.add("disabled");
|
202
244
|
}
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
document.querySelector('button.buttons-select-all').classList.add('disabled');
|
245
|
+
if (dataTable.rows({ selected: true }).count() === 1) {
|
246
|
+
document.querySelector("button.buttons-logs").classList.remove("disabled");
|
247
|
+
document.querySelector("button.buttons-rename").classList.remove("disabled");
|
207
248
|
} else {
|
208
|
-
document.querySelector(
|
249
|
+
document.querySelector("button.buttons-logs").classList.add("disabled");
|
250
|
+
document.querySelector("button.buttons-rename").classList.add("disabled");
|
209
251
|
}
|
210
252
|
}
|
211
253
|
|
212
|
-
function
|
213
|
-
const
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
})
|
222
|
-
.then(data => {
|
223
|
-
selectElem = document.getElementById("device-selected-fw");
|
254
|
+
async function updateDeviceConfig() {
|
255
|
+
const devices = dataTable
|
256
|
+
.rows({ selected: true })
|
257
|
+
.data()
|
258
|
+
.toArray()
|
259
|
+
.map((d) => d.uuid);
|
260
|
+
const firmware = document.getElementById("selected-fw").value;
|
261
|
+
const feed = document.getElementById("rollout-selected-feed").value;
|
262
|
+
const flavor = document.getElementById("rollout-selected-flavor").value;
|
224
263
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
optionElem = document.createElement("option");
|
231
|
-
optionElem.value = "latest";
|
232
|
-
optionElem.textContent = "latest";
|
233
|
-
selectElem.appendChild(optionElem);
|
234
|
-
|
235
|
-
data.forEach(item => {
|
236
|
-
optionElem = document.createElement("option");
|
237
|
-
optionElem.value = item["name"];
|
238
|
-
optionElem.textContent = item["name"];
|
239
|
-
selectElem.appendChild(optionElem);
|
240
|
-
});
|
241
|
-
})
|
242
|
-
.catch(error => {
|
243
|
-
console.error('Failed to fetch device data:', error);
|
244
|
-
});
|
245
|
-
}
|
246
|
-
|
247
|
-
function updateDeviceConfig() {
|
248
|
-
selectedDevices = dataTable.rows( {selected:true} ).data().toArray().map(d => d["uuid"]);
|
249
|
-
selectedFirmware = document.getElementById("device-selected-fw").value;
|
250
|
-
|
251
|
-
fetch('/api/devices/update', {
|
252
|
-
method: 'POST',
|
253
|
-
headers: {
|
254
|
-
'Content-Type': 'application/json'
|
255
|
-
},
|
256
|
-
body: JSON.stringify({
|
257
|
-
'devices': selectedDevices,
|
258
|
-
'firmware': selectedFirmware
|
259
|
-
})
|
260
|
-
}).then(response => {
|
261
|
-
if (!response.ok) {
|
262
|
-
throw new Error('Failed to update devices.');
|
263
|
-
}
|
264
|
-
return response.json();
|
265
|
-
}).catch(error => {
|
266
|
-
console.error('Error:', error);
|
267
|
-
});
|
264
|
+
try {
|
265
|
+
await post("/api/devices/update", { devices, firmware, feed, flavor });
|
266
|
+
} catch (error) {
|
267
|
+
console.error("Update device config failed:", error);
|
268
|
+
}
|
268
269
|
|
269
270
|
setTimeout(updateDeviceList, 50);
|
270
271
|
}
|
271
272
|
|
272
|
-
function updateDeviceName() {
|
273
|
-
|
274
|
-
|
273
|
+
async function updateDeviceName() {
|
274
|
+
const devices = dataTable
|
275
|
+
.rows({ selected: true })
|
276
|
+
.data()
|
277
|
+
.toArray()
|
278
|
+
.map((d) => d.uuid);
|
279
|
+
const name = document.getElementById("device-selected-name").value;
|
275
280
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
body: JSON.stringify({
|
282
|
-
'devices': selectedDevices,
|
283
|
-
'name': name
|
284
|
-
})
|
285
|
-
}).then(response => {
|
286
|
-
if (!response.ok) {
|
287
|
-
throw new Error('Failed to update devices.');
|
288
|
-
}
|
289
|
-
return response.json();
|
290
|
-
}).catch(error => {
|
291
|
-
console.error('Error:', error);
|
292
|
-
});
|
281
|
+
try {
|
282
|
+
await post("/api/devices/update", { devices, name });
|
283
|
+
} catch (error) {
|
284
|
+
console.error("Update device name failed:", error);
|
285
|
+
}
|
293
286
|
|
294
287
|
setTimeout(updateDeviceList, 50);
|
295
288
|
}
|
296
289
|
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
},
|
304
|
-
body: JSON.stringify({
|
305
|
-
'devices': devices,
|
306
|
-
})
|
307
|
-
}).then(response => {
|
308
|
-
if (!response.ok) {
|
309
|
-
throw new Error('Failed to force device update.');
|
310
|
-
}
|
311
|
-
return response.json();
|
312
|
-
}).catch(error => {
|
313
|
-
console.error('Error:', error);
|
314
|
-
});
|
290
|
+
async function forceUpdateDevices(devices) {
|
291
|
+
try {
|
292
|
+
await post("/api/devices/force_update", { devices });
|
293
|
+
} catch (error) {
|
294
|
+
console.error("Update force update state failed:", error);
|
295
|
+
}
|
315
296
|
|
316
297
|
setTimeout(updateDeviceList, 50);
|
317
298
|
}
|
318
299
|
|
319
|
-
function deleteDevices(devices) {
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
body: JSON.stringify({
|
326
|
-
'devices': devices,
|
327
|
-
})
|
328
|
-
}).then(response => {
|
329
|
-
if (!response.ok) {
|
330
|
-
throw new Error('Failed to delete devices.');
|
331
|
-
}
|
332
|
-
return response.json();
|
333
|
-
}).catch(error => {
|
334
|
-
console.error('Error:', error);
|
335
|
-
});
|
300
|
+
async function deleteDevices(devices) {
|
301
|
+
try {
|
302
|
+
await post("/api/devices/delete", { devices });
|
303
|
+
} catch (error) {
|
304
|
+
console.error("Delete device failed:", error);
|
305
|
+
}
|
336
306
|
|
337
307
|
setTimeout(updateDeviceList, 50);
|
338
308
|
}
|
339
309
|
|
340
|
-
function pinDevices(devices) {
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
body: JSON.stringify({
|
347
|
-
'devices': devices,
|
348
|
-
'firmware': "pinned"
|
349
|
-
})
|
350
|
-
}).then(response => {
|
351
|
-
if (!response.ok) {
|
352
|
-
throw new Error('Failed to update devices.');
|
353
|
-
}
|
354
|
-
return response.json();
|
355
|
-
}).catch(error => {
|
356
|
-
console.error('Error:', error);
|
357
|
-
});
|
310
|
+
async function pinDevices(devices) {
|
311
|
+
try {
|
312
|
+
await post("/api/devices/update", { devices, pinned: true });
|
313
|
+
} catch (error) {
|
314
|
+
console.error("Error:", error);
|
315
|
+
}
|
358
316
|
|
359
317
|
setTimeout(updateDeviceList, 50);
|
360
318
|
}
|
@@ -362,24 +320,3 @@ function pinDevices(devices) {
|
|
362
320
|
function updateDeviceList() {
|
363
321
|
dataTable.ajax.reload();
|
364
322
|
}
|
365
|
-
|
366
|
-
function secondsToRecentDate(t) {
|
367
|
-
if (t == null) {
|
368
|
-
return null
|
369
|
-
}
|
370
|
-
t = Number(t);
|
371
|
-
var d = Math.floor(t / 86400)
|
372
|
-
var h = Math.floor(t % 86400 / 3600);
|
373
|
-
var m = Math.floor(t % 86400 % 3600 / 60);
|
374
|
-
var s = Math.floor(t % 86400 % 3600 % 60);
|
375
|
-
|
376
|
-
if (d > 0) {
|
377
|
-
return d + (d == 1 ? " day" : " days");
|
378
|
-
} else if (h > 0) {
|
379
|
-
return h + (h == 1 ? " hour" : " hours");
|
380
|
-
} else if (m > 0) {
|
381
|
-
return m + (m == 1 ? " minute" : " minutes");
|
382
|
-
} else {
|
383
|
-
return s + (s == 1 ? " second" : " seconds");
|
384
|
-
}
|
385
|
-
}
|