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
goosebit/ui/static/js/devices.js
CHANGED
@@ -1,72 +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:
|
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" },
|
44
70
|
{
|
45
|
-
data:
|
46
|
-
render:
|
47
|
-
if (
|
48
|
-
color = data ? "success" : "
|
71
|
+
data: "force_update",
|
72
|
+
render: (data, type) => {
|
73
|
+
if (type === "display" || type === "filter") {
|
74
|
+
const color = data ? "success" : "muted";
|
49
75
|
return `
|
50
76
|
<div class="text-${color}">
|
51
77
|
●
|
52
78
|
</div>
|
53
|
-
|
79
|
+
`;
|
54
80
|
}
|
55
81
|
return data;
|
56
|
-
}
|
82
|
+
},
|
57
83
|
},
|
58
|
-
{ data: 'fw_file' },
|
59
|
-
{ data: 'last_ip' },
|
60
84
|
{
|
61
|
-
data:
|
62
|
-
render:
|
63
|
-
if (
|
85
|
+
data: "progress",
|
86
|
+
render: (data, type) => {
|
87
|
+
if (type === "display" || type === "filter") {
|
88
|
+
return data ? `${data}%` : "-";
|
89
|
+
}
|
90
|
+
return data;
|
91
|
+
},
|
92
|
+
},
|
93
|
+
{ data: "last_ip" },
|
94
|
+
{
|
95
|
+
data: "last_seen",
|
96
|
+
render: (data, type) => {
|
97
|
+
if (type === "display" || type === "filter") {
|
64
98
|
return secondsToRecentDate(data);
|
65
99
|
}
|
66
100
|
return data;
|
67
|
-
}
|
101
|
+
},
|
68
102
|
},
|
69
|
-
{ data: 'state' },
|
70
103
|
],
|
71
104
|
layout: {
|
72
105
|
top1Start: {
|
@@ -74,272 +107,212 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
74
107
|
{
|
75
108
|
text: '<i class="bi bi-check-all"></i>',
|
76
109
|
extend: "selectAll",
|
77
|
-
titleAttr:
|
110
|
+
titleAttr: "Select All",
|
78
111
|
},
|
79
112
|
{
|
80
113
|
text: '<i class="bi bi-x"></i>',
|
81
114
|
extend: "selectNone",
|
82
|
-
titleAttr:
|
115
|
+
titleAttr: "Clear Selection",
|
83
116
|
},
|
84
117
|
{
|
85
118
|
text: '<i class="bi bi-file-text"></i>',
|
86
|
-
action:
|
87
|
-
selectedDevice = dataTable.rows(
|
88
|
-
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}`;
|
89
122
|
},
|
90
123
|
className: "buttons-logs",
|
91
|
-
titleAttr:
|
124
|
+
titleAttr: "View Log",
|
92
125
|
},
|
93
|
-
]
|
126
|
+
],
|
94
127
|
},
|
95
128
|
bottom1Start: {
|
96
129
|
buttons: [
|
97
130
|
{
|
98
131
|
text: '<i class="bi bi-pen" ></i>',
|
99
|
-
action:
|
132
|
+
action: (e, dt) => {
|
100
133
|
const input = document.getElementById("device-selected-name");
|
101
|
-
|
102
|
-
|
134
|
+
input.value = dt
|
135
|
+
.rows({ selected: true })
|
136
|
+
.data()
|
137
|
+
.toArray()
|
138
|
+
.map((d) => d.name)[0];
|
103
139
|
|
104
|
-
new bootstrap.Modal(
|
140
|
+
new bootstrap.Modal("#device-rename-modal").show();
|
105
141
|
},
|
106
142
|
className: "buttons-rename",
|
107
|
-
titleAttr:
|
143
|
+
titleAttr: "Rename Devices",
|
108
144
|
},
|
109
145
|
{
|
110
146
|
text: '<i class="bi bi-gear" ></i>',
|
111
|
-
action:
|
112
|
-
|
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();
|
113
163
|
},
|
114
164
|
className: "buttons-config",
|
115
|
-
titleAttr:
|
165
|
+
titleAttr: "Configure Devices",
|
116
166
|
},
|
117
167
|
{
|
118
168
|
text: '<i class="bi bi-trash" ></i>',
|
119
|
-
action:
|
120
|
-
selectedDevices = dt
|
169
|
+
action: (e, dt) => {
|
170
|
+
const selectedDevices = dt
|
171
|
+
.rows({ selected: true })
|
172
|
+
.data()
|
173
|
+
.toArray()
|
174
|
+
.map((d) => d.uuid);
|
121
175
|
deleteDevices(selectedDevices);
|
122
176
|
},
|
123
177
|
className: "buttons-delete",
|
124
|
-
titleAttr:
|
178
|
+
titleAttr: "Delete Devices",
|
125
179
|
},
|
126
180
|
{
|
127
181
|
text: '<i class="bi bi-box-arrow-in-up-right"></i>',
|
128
|
-
action:
|
129
|
-
selectedDevices = dataTable
|
182
|
+
action: () => {
|
183
|
+
const selectedDevices = dataTable
|
184
|
+
.rows({ selected: true })
|
185
|
+
.data()
|
186
|
+
.toArray()
|
187
|
+
.map((d) => d.uuid);
|
130
188
|
forceUpdateDevices(selectedDevices);
|
131
189
|
},
|
132
190
|
className: "buttons-force-update",
|
133
|
-
titleAttr:
|
191
|
+
titleAttr: "Force Update",
|
134
192
|
},
|
135
193
|
{
|
136
194
|
text: '<i class="bi bi-pin-angle"></i>',
|
137
|
-
action:
|
138
|
-
selectedDevices = dataTable
|
195
|
+
action: () => {
|
196
|
+
const selectedDevices = dataTable
|
197
|
+
.rows({ selected: true })
|
198
|
+
.data()
|
199
|
+
.toArray()
|
200
|
+
.map((d) => d.uuid);
|
139
201
|
pinDevices(selectedDevices);
|
140
202
|
},
|
141
203
|
className: "buttons-pin",
|
142
|
-
titleAttr:
|
204
|
+
titleAttr: "Pin Version",
|
143
205
|
},
|
144
|
-
]
|
145
|
-
}
|
206
|
+
],
|
207
|
+
},
|
146
208
|
},
|
147
209
|
});
|
148
210
|
|
149
|
-
dataTable.on(
|
150
|
-
|
151
|
-
|
152
|
-
updateDeviceName(uuid);
|
211
|
+
dataTable.on("click", "button.edit-name", (e) => {
|
212
|
+
const data = dataTable.row(e.target.closest("tr")).data();
|
213
|
+
updateDeviceName(data.uuid);
|
153
214
|
});
|
154
215
|
|
155
|
-
dataTable
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
216
|
+
dataTable
|
217
|
+
.on("select", () => {
|
218
|
+
updateBtnState();
|
219
|
+
})
|
220
|
+
.on("deselect", () => {
|
221
|
+
updateBtnState();
|
222
|
+
});
|
160
223
|
|
161
|
-
setInterval(
|
224
|
+
setInterval(() => {
|
162
225
|
dataTable.ajax.reload(null, false);
|
163
226
|
}, TABLE_UPDATE_TIME);
|
164
227
|
|
165
|
-
updateFirmwareSelection();
|
228
|
+
await updateFirmwareSelection(true);
|
166
229
|
});
|
167
230
|
|
168
|
-
|
169
231
|
function updateBtnState() {
|
170
|
-
dataTable
|
171
|
-
|
172
|
-
document.querySelector(
|
173
|
-
document.querySelector(
|
174
|
-
document.querySelector(
|
175
|
-
document.querySelector(
|
176
|
-
document.querySelector('button.buttons-pin').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");
|
177
238
|
} else {
|
178
|
-
document.querySelector(
|
179
|
-
document.querySelector(
|
180
|
-
document.querySelector(
|
181
|
-
document.querySelector(
|
182
|
-
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");
|
183
244
|
}
|
184
|
-
if (dataTable.rows(
|
185
|
-
document.querySelector(
|
186
|
-
document.querySelector(
|
187
|
-
} else {
|
188
|
-
document.querySelector('button.buttons-logs').classList.add('disabled');
|
189
|
-
document.querySelector('button.buttons-rename').classList.add('disabled');
|
190
|
-
}
|
191
|
-
|
192
|
-
|
193
|
-
if(dataTable.rows( {selected:true} ).ids().toArray().length === dataTable.rows().ids().toArray().length){
|
194
|
-
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");
|
195
248
|
} else {
|
196
|
-
document.querySelector(
|
249
|
+
document.querySelector("button.buttons-logs").classList.add("disabled");
|
250
|
+
document.querySelector("button.buttons-rename").classList.add("disabled");
|
197
251
|
}
|
198
252
|
}
|
199
253
|
|
200
|
-
function
|
201
|
-
const
|
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;
|
202
263
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
return response.json();
|
209
|
-
})
|
210
|
-
.then(data => {
|
211
|
-
selectElem = document.getElementById("device-selected-fw");
|
212
|
-
|
213
|
-
optionElem = document.createElement("option");
|
214
|
-
optionElem.value = "latest";
|
215
|
-
optionElem.textContent = "latest";
|
216
|
-
|
217
|
-
selectElem.appendChild(optionElem);
|
218
|
-
|
219
|
-
data.forEach(item => {
|
220
|
-
optionElem = document.createElement("option");
|
221
|
-
optionElem.value = item["name"];
|
222
|
-
optionElem.textContent = item["name"];
|
223
|
-
|
224
|
-
selectElem.appendChild(optionElem);
|
225
|
-
});
|
226
|
-
})
|
227
|
-
.catch(error => {
|
228
|
-
console.error('Failed to fetch device data:', error);
|
229
|
-
});
|
230
|
-
}
|
231
|
-
|
232
|
-
function updateDeviceConfig() {
|
233
|
-
selectedDevices = dataTable.rows( {selected:true} ).data().toArray().map(d => d["uuid"]);
|
234
|
-
selectedFirmware = document.getElementById("device-selected-fw").value;
|
235
|
-
|
236
|
-
fetch('/api/devices/update', {
|
237
|
-
method: 'POST',
|
238
|
-
headers: {
|
239
|
-
'Content-Type': 'application/json'
|
240
|
-
},
|
241
|
-
body: JSON.stringify({
|
242
|
-
'devices': selectedDevices,
|
243
|
-
'firmware': selectedFirmware
|
244
|
-
})
|
245
|
-
}).then(response => {
|
246
|
-
if (!response.ok) {
|
247
|
-
throw new Error('Failed to update devices.');
|
248
|
-
}
|
249
|
-
return response.json();
|
250
|
-
}).catch(error => {
|
251
|
-
console.error('Error:', error);
|
252
|
-
});
|
264
|
+
try {
|
265
|
+
await post("/api/devices/update", { devices, firmware, feed, flavor });
|
266
|
+
} catch (error) {
|
267
|
+
console.error("Update device config failed:", error);
|
268
|
+
}
|
253
269
|
|
254
270
|
setTimeout(updateDeviceList, 50);
|
255
271
|
}
|
256
272
|
|
257
|
-
function updateDeviceName() {
|
258
|
-
|
259
|
-
|
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;
|
260
280
|
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
body: JSON.stringify({
|
267
|
-
'devices': selectedDevices,
|
268
|
-
'name': name
|
269
|
-
})
|
270
|
-
}).then(response => {
|
271
|
-
if (!response.ok) {
|
272
|
-
throw new Error('Failed to update devices.');
|
273
|
-
}
|
274
|
-
return response.json();
|
275
|
-
}).catch(error => {
|
276
|
-
console.error('Error:', error);
|
277
|
-
});
|
281
|
+
try {
|
282
|
+
await post("/api/devices/update", { devices, name });
|
283
|
+
} catch (error) {
|
284
|
+
console.error("Update device name failed:", error);
|
285
|
+
}
|
278
286
|
|
279
287
|
setTimeout(updateDeviceList, 50);
|
280
288
|
}
|
281
289
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
},
|
289
|
-
body: JSON.stringify({
|
290
|
-
'devices': devices,
|
291
|
-
})
|
292
|
-
}).then(response => {
|
293
|
-
if (!response.ok) {
|
294
|
-
throw new Error('Failed to force device update.');
|
295
|
-
}
|
296
|
-
return response.json();
|
297
|
-
}).catch(error => {
|
298
|
-
console.error('Error:', error);
|
299
|
-
});
|
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
|
+
}
|
300
296
|
|
301
297
|
setTimeout(updateDeviceList, 50);
|
302
298
|
}
|
303
299
|
|
304
|
-
function deleteDevices(devices) {
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
body: JSON.stringify({
|
311
|
-
'devices': devices,
|
312
|
-
})
|
313
|
-
}).then(response => {
|
314
|
-
if (!response.ok) {
|
315
|
-
throw new Error('Failed to delete devices.');
|
316
|
-
}
|
317
|
-
return response.json();
|
318
|
-
}).catch(error => {
|
319
|
-
console.error('Error:', error);
|
320
|
-
});
|
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
|
+
}
|
321
306
|
|
322
307
|
setTimeout(updateDeviceList, 50);
|
323
308
|
}
|
324
309
|
|
325
|
-
function pinDevices(devices) {
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
body: JSON.stringify({
|
332
|
-
'devices': devices,
|
333
|
-
'firmware': "pinned"
|
334
|
-
})
|
335
|
-
}).then(response => {
|
336
|
-
if (!response.ok) {
|
337
|
-
throw new Error('Failed to update devices.');
|
338
|
-
}
|
339
|
-
return response.json();
|
340
|
-
}).catch(error => {
|
341
|
-
console.error('Error:', error);
|
342
|
-
});
|
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
|
+
}
|
343
316
|
|
344
317
|
setTimeout(updateDeviceList, 50);
|
345
318
|
}
|
@@ -347,24 +320,3 @@ function pinDevices(devices) {
|
|
347
320
|
function updateDeviceList() {
|
348
321
|
dataTable.ajax.reload();
|
349
322
|
}
|
350
|
-
|
351
|
-
function secondsToRecentDate(t) {
|
352
|
-
if (t == null) {
|
353
|
-
return null
|
354
|
-
}
|
355
|
-
t = Number(t);
|
356
|
-
var d = Math.floor(t / 86400)
|
357
|
-
var h = Math.floor(t % 86400 / 3600);
|
358
|
-
var m = Math.floor(t % 86400 % 3600 / 60);
|
359
|
-
var s = Math.floor(t % 86400 % 3600 % 60);
|
360
|
-
|
361
|
-
if (d > 0) {
|
362
|
-
return d + (d == 1 ? " day" : " days");
|
363
|
-
} else if (h > 0) {
|
364
|
-
return h + (h == 1 ? " hour" : " hours");
|
365
|
-
} else if (m > 0) {
|
366
|
-
return m + (m == 1 ? " minute" : " minutes");
|
367
|
-
} else {
|
368
|
-
return s + (s == 1 ? " second" : " seconds");
|
369
|
-
}
|
370
|
-
}
|