goosebit 0.1.0__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 (48) hide show
  1. goosebit/__init__.py +62 -0
  2. goosebit/api/__init__.py +1 -0
  3. goosebit/api/devices.py +112 -0
  4. goosebit/api/download.py +20 -0
  5. goosebit/api/firmware.py +64 -0
  6. goosebit/api/routes.py +11 -0
  7. goosebit/auth/__init__.py +123 -0
  8. goosebit/db.py +32 -0
  9. goosebit/models.py +21 -0
  10. goosebit/permissions.py +55 -0
  11. goosebit/realtime/__init__.py +1 -0
  12. goosebit/realtime/logs.py +43 -0
  13. goosebit/realtime/routes.py +13 -0
  14. goosebit/settings.py +55 -0
  15. goosebit/ui/__init__.py +1 -0
  16. goosebit/ui/routes.py +104 -0
  17. goosebit/ui/static/__init__.py +5 -0
  18. goosebit/ui/static/favicon.ico +0 -0
  19. goosebit/ui/static/favicon.svg +1 -0
  20. goosebit/ui/static/js/devices.js +370 -0
  21. goosebit/ui/static/js/firmware.js +131 -0
  22. goosebit/ui/static/js/index.js +161 -0
  23. goosebit/ui/static/js/logs.js +18 -0
  24. goosebit/ui/static/svg/goosebit-logo.svg +1 -0
  25. goosebit/ui/templates/__init__.py +5 -0
  26. goosebit/ui/templates/devices.html +82 -0
  27. goosebit/ui/templates/firmware.html +47 -0
  28. goosebit/ui/templates/index.html +37 -0
  29. goosebit/ui/templates/login.html +34 -0
  30. goosebit/ui/templates/logs.html +21 -0
  31. goosebit/ui/templates/nav.html +64 -0
  32. goosebit/updater/__init__.py +1 -0
  33. goosebit/updater/controller/__init__.py +1 -0
  34. goosebit/updater/controller/routes.py +6 -0
  35. goosebit/updater/controller/v1/__init__.py +1 -0
  36. goosebit/updater/controller/v1/routes.py +92 -0
  37. goosebit/updater/download/__init__.py +1 -0
  38. goosebit/updater/download/routes.py +6 -0
  39. goosebit/updater/download/v1/__init__.py +1 -0
  40. goosebit/updater/download/v1/routes.py +26 -0
  41. goosebit/updater/manager.py +206 -0
  42. goosebit/updater/misc.py +69 -0
  43. goosebit/updater/routes.py +30 -0
  44. goosebit/updater/updates.py +93 -0
  45. goosebit-0.1.0.dist-info/LICENSE +201 -0
  46. goosebit-0.1.0.dist-info/METADATA +37 -0
  47. goosebit-0.1.0.dist-info/RECORD +48 -0
  48. goosebit-0.1.0.dist-info/WHEEL +4 -0
goosebit/ui/routes.py ADDED
@@ -0,0 +1,104 @@
1
+ import aiofiles
2
+ from fastapi import APIRouter, Depends, Form, Security, UploadFile
3
+ from fastapi.requests import Request
4
+ from fastapi.responses import RedirectResponse
5
+ from fastapi.security import OAuth2PasswordBearer
6
+
7
+ from goosebit.auth import authenticate_session, validate_user_permissions
8
+ from goosebit.permissions import Permissions
9
+ from goosebit.settings import UPDATES_DIR
10
+ from goosebit.ui.templates import templates
11
+
12
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
13
+
14
+ router = APIRouter(
15
+ prefix="/ui", dependencies=[Depends(authenticate_session)], include_in_schema=False
16
+ )
17
+
18
+
19
+ @router.get("/")
20
+ async def ui_root(request: Request):
21
+ return RedirectResponse(request.url_for("home_ui"))
22
+
23
+
24
+ @router.get(
25
+ "/firmware",
26
+ dependencies=[
27
+ Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
28
+ ],
29
+ )
30
+ async def firmware_ui(request: Request):
31
+ return templates.TemplateResponse(
32
+ "firmware.html", context={"request": request, "title": "Firmware"}
33
+ )
34
+
35
+
36
+ @router.post(
37
+ "/upload",
38
+ dependencies=[
39
+ Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.WRITE])
40
+ ],
41
+ )
42
+ async def upload_update(
43
+ request: Request,
44
+ chunk: UploadFile = Form(...),
45
+ init: bool = Form(...),
46
+ done: bool = Form(...),
47
+ filename: str = Form(...),
48
+ ):
49
+ file = UPDATES_DIR.joinpath(filename)
50
+ tmpfile = file.with_suffix(".tmp")
51
+ contents = await chunk.read()
52
+ if init:
53
+ file.unlink(missing_ok=True)
54
+
55
+ async with aiofiles.open(tmpfile, mode="ab") as f:
56
+ await f.write(contents)
57
+ if done:
58
+ tmpfile.replace(file)
59
+
60
+
61
+ @router.get(
62
+ "/home",
63
+ dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
64
+ )
65
+ async def home_ui(request: Request):
66
+ return templates.TemplateResponse(
67
+ "index.html", context={"request": request, "title": "Home"}
68
+ )
69
+
70
+
71
+ @router.get(
72
+ "/devices",
73
+ dependencies=[
74
+ Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
75
+ ],
76
+ )
77
+ async def devices_ui(request: Request):
78
+ return templates.TemplateResponse(
79
+ "devices.html", context={"request": request, "title": "Devices"}
80
+ )
81
+
82
+
83
+ @router.get(
84
+ "/logs/{dev_id}",
85
+ dependencies=[
86
+ Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
87
+ ],
88
+ )
89
+ async def logs_ui(request: Request, dev_id: str):
90
+ return templates.TemplateResponse(
91
+ "logs.html", context={"request": request, "title": "Log", "device": dev_id}
92
+ )
93
+
94
+
95
+ @router.get(
96
+ "/tunnels",
97
+ dependencies=[
98
+ Security(validate_user_permissions, scopes=[Permissions.TUNNEL.READ])
99
+ ],
100
+ )
101
+ async def tunnels_ui(request: Request):
102
+ return templates.TemplateResponse(
103
+ "tunnels.html", context={"request": request, "title": "Tunnels"}
104
+ )
@@ -0,0 +1,5 @@
1
+ from pathlib import Path
2
+
3
+ from fastapi.staticfiles import StaticFiles
4
+
5
+ static = StaticFiles(directory=Path(__file__).resolve().parent)
Binary file
@@ -0,0 +1 @@
1
+ <svg width="160mm" height="160mm" version="1.1" viewBox="0 0 160 160" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="translate(-32.316 -71.486)"><g transform="translate(3.1433 .77798)" stroke="#000"><path d="m112.72 77.662a3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 0.89762 2.1699v13.542h-43.886c-7.6869 0-14.133 5.0881-16.192 12.091h-12.416a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h11.731c-0.0039 0.15084-0.02274 0.29776-0.02274 0.44958v7.3003h-11.719a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h11.708v7.7499h-11.719a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h11.708v7.7499h-11.719a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h11.708v7.7499h-11.719a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h11.708v7.7499h-11.719a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89761h11.708v7.7494h-11.719a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h11.708v7.3008c0 0.1517 0.0188 0.29835 0.02274 0.44907h-11.742a3.072 3.072 0 0 0-2.159-0.89762 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 2.1699-0.89762h12.405c1.6399 5.5765 6.0655 9.9236 11.683 11.468v15.152a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.527c0.05398 5e-4 0.10609 8e-3 0.1602 8e-3h7.5897v14.529a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.518h7.7494v14.529a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.518h7.7499v14.529a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.518h7.7499v14.529a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.518h7.7499v14.529a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.518h7.7499v14.529a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-14.518h7.5892c0.0543 0 0.10656-8e-3 0.16071-8e-3v14.538a3.072 3.072 0 0 0-0.89762 2.159 3.072 3.072 0 0 0 3.0722 3.0722 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-0.89762-2.1699v-15.141c5.6174-1.5445 10.043-5.8916 11.682-11.468h12.992a3.072 3.072 0 0 0 2.159 0.89762 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-2.1699 0.89762h-12.307c4e-3 -0.15072 0.0227-0.29737 0.0227-0.44907v-7.3008h12.295a3.072 3.072 0 0 0 2.159 0.89762 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-2.1699 0.89762h-12.284v-7.7494h12.295a3.072 3.072 0 0 0 2.159 0.89761 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-2.1699 0.89762h-12.284v-7.7499h12.295a3.072 3.072 0 0 0 2.159 0.89762 3.072 3.072 0 0 0 3.0722-3.0722 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-2.1699 0.89762h-12.284v-43.597c0-7.791-5.2299-14.3-12.38-16.266v-14.176a3.072 3.072 0 0 0 0.89762-2.159 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 0.89762 2.1699v13.551c-0.0542-5.05e-4 -0.10644-0.0083-0.16071-0.0083h-7.5892v-13.553a3.072 3.072 0 0 0 0.89762-2.159 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 0.89762 2.1699v13.542h-7.7499v-13.553a3.072 3.072 0 0 0 0.89762-2.159 3.072 3.072 0 0 0-3.0722-3.0722 3.072 3.072 0 0 0-3.0722 3.0722 3.072 3.072 0 0 0 0.89762 2.1699v13.542h-7.7499v-13.553a3.072 3.072 0 0 0 0.89762-2.159 3.072 3.072 0 0 0-3.0722-3.0722z" fill="#fff"/><g><path d="m83.315 71.988c-7.7785-0.10611-12.693 4.0778-12.693 4.0778s-4.8045 3.244-6.8197 5.6885c-2.0152 2.4446-3.9998 3.3161-3.9998 3.3161l-10.92 4.4292s-4.3039 2.004-4.6829 6.8874c-0.37903 4.8834 8.6052 6.8151 8.6052 6.8151l10.779 1.2867c-3.797 2.1848-6.349 6.2714-6.349 10.984v74.649c0 7.0224 5.6533 12.676 12.676 12.676h74.649c7.0224 0 12.676-5.6533 12.676-12.676v-28.141c19.08-22.042 22.782-33.839 22.782-33.839s2.3205-7.1538 4.8922-15.174c2.5717-8.0199-3.6974-6.8585-3.6974-6.8585l-24.252 6.7293c-1.207-5.7485-6.2825-10.041-12.4-10.041h-33.177c-1.9266-19.83-13.78-25.894-13.78-25.894-5.3521-3.6273-10.213-4.8599-14.287-4.9155z" fill="#121e2c"/><path d="m69.443 107.99c-3.9647 0-7.1567 3.1915-7.1567 7.1562v40.219c0.96019-8.4807 4.4328-18.052 9.6733-24.824 9.8714-12.755 9.3931-19.262 5.7521-22.552zm42.644 0c0.24496 10.488-4.3418 20.083-4.3418 20.083l44.46-13.835c-0.44487-3.5314-3.4372-6.2477-7.0931-6.2477zm-49.801 61.069v21.844c0 3.9647 3.192 7.1562 7.1567 7.1562h18.112c-8.3075-3.693-21.415-12.054-25.268-29z" fill="#29befe"/><path d="m178.96 112.14-69.703 20.837s-18.894 12.226-6.2239 35.914c0.31428-4e-3 39.979 0.0384 39.979 0.0384s5.3864-3.9688 9.3648-9.8406l-40.695-0.25269s-2.0754 0.0793-2.0075-2.3201c0.068-2.3994 1.2177-2.4825 1.2177-2.4825l44.832 0.25605s3.3352-2.6554 8.4851-11.307l-46.34-0.0399s-1.8324-0.34051-1.7768-2.6268c0.0556-2.2863 2.6305-2.585 2.6305-2.585l45.957-0.17048s2.2148 0.37311 4.94-2.6224c2.7252-2.9954 9.801-21.321 9.801-21.321s0.63122-1.6448-0.46214-1.4781zm-72.26 60.929 33.354-0.22045s-4.43 6.7245-14.346 7.0264c-14.503 0.44144-19.008-6.8059-19.008-6.8059zm-39.296-87.393 7.3033 15.24s22.892 2.1374 4.4586 29.071c-18.433 26.934-7.9015 44.329-7.9015 44.329s10.309 21.075 36.028 23.294l35.722 0.45927s7.6669 5e-3 8.9801-7.2795l-0.0255-22.663s-10.703 16.455-23.723 17.408c-9.6849 0.70923-14.934-1.1038-17.672-2.5611-2.2513-1.1981-30.046-14.686-11.338-50.201 16.735-31.77-0.73572-47.166-0.73572-47.166l-13.986 3.0599s0.67535 2.9676-1.8951 4.436c-1.9916 1.1377-5.4854 0.11131-6.6183-1.8831-1.1522-2.0285-0.22903-5.5118 1.8024-6.7625 2.7445-1.6898 5.1812-0.15486 5.1812-0.15486l12.185-2.4294s-15.541-10.99-27.765 3.8033zm2.126 14.889s-16.877-2.0953-19.634-2.9542c-2.7568-0.85892-0.37994-2.5393-0.37994-2.5393s10.175-2.8442 14.302-6.5923z" fill="#fff"/></g></g></g></svg>
@@ -0,0 +1,370 @@
1
+ document.addEventListener("DOMContentLoaded", function() {
2
+ var dataTable = new DataTable("#device-table", {
3
+ responsive: true,
4
+ paging: false,
5
+ scrollCollapse: true,
6
+ scroller: true,
7
+ scrollY: "65vh",
8
+ stateSave: true,
9
+ select: true,
10
+ rowId: "uuid",
11
+ ajax: {
12
+ url: "/api/devices/all",
13
+ dataSrc: "",
14
+ },
15
+ initComplete:function(){
16
+ updateBtnState();
17
+ },
18
+ columnDefs: [
19
+ {
20
+ targets: "_all",
21
+ render: function(data, type, row) {
22
+ return data || "❓";
23
+ },
24
+ }
25
+ ],
26
+ columns: [
27
+ { data: 'name' },
28
+ {
29
+ data: 'online',
30
+ render: function(data, type, row) {
31
+ if ( type === 'display' || type === 'filter' ) {
32
+ color = data ? "success" : "danger"
33
+ return `
34
+ <div class="text-${color}">
35
+
36
+ </div>
37
+ `
38
+ }
39
+ return data;
40
+ }
41
+ },
42
+ { data: 'uuid' },
43
+ { data: 'fw' },
44
+ {
45
+ data: 'force_update',
46
+ render: function(data, type, row) {
47
+ if ( type === 'display' || type === 'filter' ) {
48
+ color = data ? "success" : "danger"
49
+ return `
50
+ <div class="text-${color}">
51
+
52
+ </div>
53
+ `
54
+ }
55
+ return data;
56
+ }
57
+ },
58
+ { data: 'fw_file' },
59
+ { data: 'last_ip' },
60
+ {
61
+ data: 'last_seen',
62
+ render: function(data, type, row) {
63
+ if ( type === 'display' || type === 'filter' ) {
64
+ return secondsToRecentDate(data);
65
+ }
66
+ return data;
67
+ }
68
+ },
69
+ { data: 'state' },
70
+ ],
71
+ layout: {
72
+ top1Start: {
73
+ buttons: [
74
+ {
75
+ text: '<i class="bi bi-check-all"></i>',
76
+ extend: "selectAll",
77
+ titleAttr: 'Select All'
78
+ },
79
+ {
80
+ text: '<i class="bi bi-x"></i>',
81
+ extend: "selectNone",
82
+ titleAttr: 'Clear Selection'
83
+ },
84
+ {
85
+ text: '<i class="bi bi-file-text"></i>',
86
+ action: function (e, dt, node, config) {
87
+ selectedDevice = dataTable.rows( {selected:true} ).data().toArray()[0];
88
+ window.location.href = `/ui/logs/${selectedDevice["uuid"]}`;
89
+ },
90
+ className: "buttons-logs",
91
+ titleAttr: 'View Log'
92
+ },
93
+ ]
94
+ },
95
+ bottom1Start: {
96
+ buttons: [
97
+ {
98
+ text: '<i class="bi bi-pen" ></i>',
99
+ action: function (e, dt, node, config) {
100
+ const input = document.getElementById("device-selected-name");
101
+ const selectedDeviceName = dt.rows( {selected:true} ).data().toArray().map(d => d["name"])[0];
102
+ input.value = selectedDeviceName;
103
+
104
+ new bootstrap.Modal('#device-rename-modal').show();
105
+ },
106
+ className: "buttons-rename",
107
+ titleAttr: 'Rename Devices'
108
+ },
109
+ {
110
+ text: '<i class="bi bi-gear" ></i>',
111
+ action: function (e, dt, node, config) {
112
+ new bootstrap.Modal('#device-config-modal').show();
113
+ },
114
+ className: "buttons-config",
115
+ titleAttr: 'Configure Devices'
116
+ },
117
+ {
118
+ text: '<i class="bi bi-trash" ></i>',
119
+ action: function (e, dt, node, config) {
120
+ selectedDevices = dt.rows( {selected:true} ).data().toArray().map(d => d["uuid"]);
121
+ deleteDevices(selectedDevices);
122
+ },
123
+ className: "buttons-delete",
124
+ titleAttr: 'Delete Devices'
125
+ },
126
+ {
127
+ text: '<i class="bi bi-box-arrow-in-up-right"></i>',
128
+ action: function (e, dt, node, config) {
129
+ selectedDevices = dataTable.rows( {selected:true} ).data().toArray().map(d => d["uuid"]);
130
+ forceUpdateDevices(selectedDevices);
131
+ },
132
+ className: "buttons-force-update",
133
+ titleAttr: 'Force Update'
134
+ },
135
+ {
136
+ text: '<i class="bi bi-pin-angle"></i>',
137
+ action: function (e, dt, node, config) {
138
+ selectedDevices = dataTable.rows( {selected:true} ).data().toArray().map(d => d["uuid"]);
139
+ pinDevices(selectedDevices);
140
+ },
141
+ className: "buttons-pin",
142
+ titleAttr: 'Pin Version'
143
+ },
144
+ ]
145
+ }
146
+ },
147
+ });
148
+
149
+ dataTable.on('click', 'button.edit-name', function (e) {
150
+ let data = dataTable.row(e.target.closest('tr')).data();
151
+ uuid = data["uuid"];
152
+ updateDeviceName(uuid);
153
+ });
154
+
155
+ dataTable.on( 'select', function ( e, dt, type, indexes ) {
156
+ updateBtnState();
157
+ } ).on( 'deselect', function ( e, dt, type, indexes ) {
158
+ updateBtnState();
159
+ } );
160
+
161
+ setInterval(function () {
162
+ dataTable.ajax.reload(null, false);
163
+ }, TABLE_UPDATE_TIME);
164
+
165
+ updateFirmwareSelection();
166
+ });
167
+
168
+
169
+ function updateBtnState() {
170
+ dataTable = $("#device-table").DataTable();
171
+ if (dataTable.rows( {selected:true} ).any()){
172
+ document.querySelector('button.buttons-select-none').classList.remove('disabled');
173
+ document.querySelector('button.buttons-config').classList.remove('disabled');
174
+ document.querySelector('button.buttons-force-update').classList.remove('disabled');
175
+ document.querySelector('button.buttons-delete').classList.remove('disabled');
176
+ document.querySelector('button.buttons-pin').classList.remove('disabled');
177
+ } else {
178
+ document.querySelector('button.buttons-select-none').classList.add('disabled');
179
+ document.querySelector('button.buttons-config').classList.add('disabled');
180
+ document.querySelector('button.buttons-force-update').classList.add('disabled');
181
+ document.querySelector('button.buttons-delete').classList.add('disabled');
182
+ document.querySelector('button.buttons-pin').classList.add('disabled');
183
+ }
184
+ if (dataTable.rows( {selected:true} ).count() == 1){
185
+ document.querySelector('button.buttons-logs').classList.remove('disabled');
186
+ document.querySelector('button.buttons-rename').classList.remove('disabled');
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');
195
+ } else {
196
+ document.querySelector('button.buttons-select-all').classList.remove('disabled');
197
+ }
198
+ }
199
+
200
+ function updateFirmwareSelection() {
201
+ const url = '/api/firmware/all';
202
+
203
+ fetch(url)
204
+ .then(response => {
205
+ if (!response.ok) {
206
+ throw new Error('Request failed');
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
+ });
253
+
254
+ setTimeout(updateDeviceList, 50);
255
+ }
256
+
257
+ function updateDeviceName() {
258
+ selectedDevices = dataTable.rows( {selected:true} ).data().toArray().map(d => d["uuid"]);
259
+ name = document.getElementById("device-selected-name").value;
260
+
261
+ fetch('/api/devices/update', {
262
+ method: 'POST',
263
+ headers: {
264
+ 'Content-Type': 'application/json'
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
+ });
278
+
279
+ setTimeout(updateDeviceList, 50);
280
+ }
281
+
282
+
283
+ function forceUpdateDevices(devices) {
284
+ fetch('/api/devices/force_update', {
285
+ method: 'POST',
286
+ headers: {
287
+ 'Content-Type': 'application/json'
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
+ });
300
+
301
+ setTimeout(updateDeviceList, 50);
302
+ }
303
+
304
+ function deleteDevices(devices) {
305
+ fetch('/api/devices/delete', {
306
+ method: 'POST',
307
+ headers: {
308
+ 'Content-Type': 'application/json'
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
+ });
321
+
322
+ setTimeout(updateDeviceList, 50);
323
+ }
324
+
325
+ function pinDevices(devices) {
326
+ fetch('/api/devices/update', {
327
+ method: 'POST',
328
+ headers: {
329
+ 'Content-Type': 'application/json'
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
+ });
343
+
344
+ setTimeout(updateDeviceList, 50);
345
+ }
346
+
347
+ function updateDeviceList() {
348
+ dataTable.ajax.reload();
349
+ }
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
+ }
@@ -0,0 +1,131 @@
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');
6
+
7
+ form.addEventListener('submit', e => {
8
+ e.preventDefault();
9
+ sendFileChunks(fileInput.files[0])
10
+ });
11
+
12
+ const sendFileChunks = async (file) => {
13
+ const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
14
+ let start = 0;
15
+ let uploadedChunks = 0;
16
+
17
+ fileSubmit.disabled = true
18
+ fileInput.disabled = true
19
+
20
+ for (let i = 0; i < totalChunks; i++) {
21
+ const end = Math.min(start + CHUNK_SIZE, file.size);
22
+ const chunk = file.slice(start, end);
23
+ 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);
30
+ }
31
+
32
+ if (i == totalChunks - 1) {
33
+ formData.append('done', true);
34
+ } else {
35
+ formData.append('done', false);
36
+ }
37
+
38
+ const response = await fetch("/ui/upload", {
39
+ method: 'POST',
40
+ body: formData,
41
+ });
42
+
43
+ if (response.ok) {
44
+ uploadedChunks++;
45
+ const progress = (uploadedChunks / totalChunks) * 100;
46
+ progressBar.style.width = `${progress}%`;
47
+ progressBar.innerHTML = `${Math.round(progress)}%`;
48
+ }
49
+
50
+ start = end;
51
+ }
52
+
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();
64
+ }
65
+
66
+ document.addEventListener("DOMContentLoaded", function() {
67
+ updateFirmwareList();
68
+ });
69
+
70
+
71
+ function updateFirmwareList() {
72
+ const url = '/api/firmware/all';
73
+
74
+ fetch(url)
75
+ .then(response => {
76
+ if (!response.ok) {
77
+ throw new Error('Request failed');
78
+ }
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);
109
+ });
110
+ })
111
+ .catch(error => {
112
+ console.error('Failed to fetch firmware data:', error);
113
+ });
114
+ }
115
+
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
+ }
125
+ updateFirmwareList();
126
+ return response.json();
127
+ })
128
+ .catch(error => {
129
+ console.error('Error:', error);
130
+ });
131
+ }