goosebit 0.1.0__py3-none-any.whl → 0.1.1__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 +3 -3
- goosebit/api/devices.py +6 -2
- goosebit/api/firmware.py +2 -15
- goosebit/api/rollouts.py +36 -0
- goosebit/api/routes.py +2 -1
- goosebit/auth/__init__.py +28 -20
- goosebit/models.py +22 -3
- goosebit/permissions.py +20 -6
- goosebit/realtime/logs.py +2 -1
- goosebit/settings.py +40 -29
- goosebit/ui/routes.py +13 -9
- goosebit/ui/static/js/devices.js +17 -2
- goosebit/ui/static/js/firmware.js +10 -1
- goosebit/ui/static/js/index.js +11 -1
- goosebit/ui/static/js/logs.js +4 -0
- goosebit/ui/static/js/rollouts.js +56 -0
- goosebit/ui/templates/devices.html +9 -0
- goosebit/ui/templates/firmware.html +2 -0
- goosebit/ui/templates/index.html +3 -0
- goosebit/ui/templates/logs.html +5 -0
- goosebit/ui/templates/nav.html +4 -2
- goosebit/ui/templates/rollouts.html +53 -0
- goosebit/updater/controller/v1/routes.py +80 -10
- goosebit/updater/download/v1/routes.py +2 -15
- goosebit/updater/manager.py +76 -35
- goosebit/updater/misc.py +23 -35
- goosebit/updater/routes.py +3 -1
- goosebit/updates/__init__.py +0 -0
- goosebit/{updater/updates.py → updates/artifacts.py} +10 -14
- goosebit/updates/version.py +38 -0
- goosebit-0.1.1.dist-info/METADATA +73 -0
- goosebit-0.1.1.dist-info/RECORD +53 -0
- 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.1.dist-info}/LICENSE +0 -0
- {goosebit-0.1.0.dist-info → goosebit-0.1.1.dist-info}/WHEEL +0 -0
goosebit/__init__.py
CHANGED
@@ -49,14 +49,14 @@ async def login_ui(request: Request):
|
|
49
49
|
|
50
50
|
|
51
51
|
@app.post("/login", include_in_schema=False, dependencies=[Depends(authenticate_user)])
|
52
|
-
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
53
|
-
resp = RedirectResponse("
|
52
|
+
async def login(request: Request, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
53
|
+
resp = RedirectResponse(request.url_for("ui_root"), status_code=302)
|
54
54
|
resp.set_cookie(key="session_id", value=create_session(form_data.username))
|
55
55
|
return resp
|
56
56
|
|
57
57
|
|
58
58
|
@app.get("/logout", include_in_schema=False)
|
59
59
|
async def logout(request: Request):
|
60
|
-
resp = RedirectResponse("
|
60
|
+
resp = RedirectResponse(request.url_for("login_ui"), status_code=302)
|
61
61
|
resp.delete_cookie(key="session_id")
|
62
62
|
return resp
|
goosebit/api/devices.py
CHANGED
@@ -30,15 +30,19 @@ async def devices_get_all() -> list[dict]:
|
|
30
30
|
last_seen = round(time.time() - device.last_seen)
|
31
31
|
return {
|
32
32
|
"uuid": device.uuid,
|
33
|
-
"web_pwd": device.web_pwd,
|
34
33
|
"name": device.name,
|
35
34
|
"fw": device.fw_version,
|
36
35
|
"fw_file": device.fw_file,
|
36
|
+
"hw_model": device.hw_model,
|
37
|
+
"hw_revision": device.hw_revision,
|
38
|
+
"progress": device.progress,
|
37
39
|
"state": device.last_state,
|
38
40
|
"force_update": manager.force_update,
|
39
41
|
"last_ip": device.last_ip,
|
40
42
|
"last_seen": last_seen,
|
41
|
-
"online":
|
43
|
+
"online": (
|
44
|
+
last_seen < manager.poll_seconds if last_seen is not None else None
|
45
|
+
),
|
42
46
|
}
|
43
47
|
|
44
48
|
return list(await asyncio.gather(*[parse(d) for d in devices]))
|
goosebit/api/firmware.py
CHANGED
@@ -4,8 +4,8 @@ from fastapi.requests import Request
|
|
4
4
|
from goosebit.auth import validate_user_permissions
|
5
5
|
from goosebit.permissions import Permissions
|
6
6
|
from goosebit.settings import UPDATES_DIR
|
7
|
-
from goosebit.updater.misc import fw_sort_key
|
8
|
-
from goosebit.
|
7
|
+
from goosebit.updater.misc import fw_sort_key
|
8
|
+
from goosebit.updates.artifacts import FirmwareArtifact
|
9
9
|
|
10
10
|
router = APIRouter(prefix="/firmware")
|
11
11
|
|
@@ -37,19 +37,6 @@ async def firmware_get_all() -> list[dict]:
|
|
37
37
|
return firmware
|
38
38
|
|
39
39
|
|
40
|
-
@router.get(
|
41
|
-
"/latest",
|
42
|
-
dependencies=[
|
43
|
-
Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
|
44
|
-
],
|
45
|
-
)
|
46
|
-
async def firmware_get_latest() -> dict:
|
47
|
-
UPDATES_DIR.mkdir(parents=True, exist_ok=True)
|
48
|
-
|
49
|
-
file_data = UPDATES_DIR.joinpath(get_newest_fw())
|
50
|
-
return {"name": file_data.name, "size": file_data.stat().st_size}
|
51
|
-
|
52
|
-
|
53
40
|
@router.post(
|
54
41
|
"/delete",
|
55
42
|
dependencies=[
|
goosebit/api/rollouts.py
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Security
|
4
|
+
|
5
|
+
from goosebit.auth import validate_user_permissions
|
6
|
+
from goosebit.models import Rollout
|
7
|
+
from goosebit.permissions import Permissions
|
8
|
+
|
9
|
+
router = APIRouter(prefix="/rollouts")
|
10
|
+
|
11
|
+
|
12
|
+
@router.get(
|
13
|
+
"/all",
|
14
|
+
dependencies=[
|
15
|
+
Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])
|
16
|
+
],
|
17
|
+
)
|
18
|
+
async def rollouts_get_all() -> list[dict]:
|
19
|
+
rollouts = await Rollout.all()
|
20
|
+
|
21
|
+
def parse(rollout: Rollout) -> dict:
|
22
|
+
return {
|
23
|
+
"id": rollout.id,
|
24
|
+
"created_at": rollout.created_at,
|
25
|
+
"name": rollout.name,
|
26
|
+
"hw_model": rollout.hw_model,
|
27
|
+
"hw_revision": rollout.hw_revision,
|
28
|
+
"feed": rollout.feed,
|
29
|
+
"flavor": rollout.flavor,
|
30
|
+
"fw_file": rollout.fw_file,
|
31
|
+
"paused": rollout.paused,
|
32
|
+
"success_count": rollout.success_count,
|
33
|
+
"failure_count": rollout.failure_count,
|
34
|
+
}
|
35
|
+
|
36
|
+
return [parse(r) for r in rollouts]
|
goosebit/api/routes.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from fastapi import APIRouter, Depends
|
2
2
|
|
3
|
-
from goosebit.api import devices, download, firmware
|
3
|
+
from goosebit.api import devices, download, firmware, rollouts
|
4
4
|
from goosebit.auth import authenticate_api_session
|
5
5
|
|
6
6
|
router = APIRouter(
|
@@ -8,4 +8,5 @@ router = APIRouter(
|
|
8
8
|
)
|
9
9
|
router.include_router(firmware.router)
|
10
10
|
router.include_router(devices.router)
|
11
|
+
router.include_router(rollouts.router)
|
11
12
|
router.include_router(download.router)
|
goosebit/auth/__init__.py
CHANGED
@@ -4,7 +4,8 @@ from fastapi import Depends, HTTPException
|
|
4
4
|
from fastapi.requests import Request
|
5
5
|
from fastapi.security import SecurityScopes
|
6
6
|
from fastapi.websockets import WebSocket
|
7
|
-
from
|
7
|
+
from joserfc import jwt
|
8
|
+
from joserfc.errors import BadSignatureError
|
8
9
|
|
9
10
|
from goosebit.settings import PWD_CXT, SECRET, USERS
|
10
11
|
|
@@ -20,7 +21,7 @@ async def authenticate_user(request: Request):
|
|
20
21
|
headers={"location": str(request.url_for("login"))},
|
21
22
|
detail="Invalid credentials",
|
22
23
|
)
|
23
|
-
if not PWD_CXT.verify(
|
24
|
+
if not PWD_CXT.verify(user.hashed_pwd, password):
|
24
25
|
raise HTTPException(
|
25
26
|
status_code=302,
|
26
27
|
headers={"location": str(request.url_for("login"))},
|
@@ -29,46 +30,49 @@ async def authenticate_user(request: Request):
|
|
29
30
|
return user
|
30
31
|
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
token = jwt.encode({"email": email}, SECRET)
|
37
|
-
sessions[token] = email
|
38
|
-
return token
|
33
|
+
def create_session(username: str) -> str:
|
34
|
+
return jwt.encode(
|
35
|
+
header={"alg": "HS256"}, claims={"username": username}, key=SECRET
|
36
|
+
)
|
39
37
|
|
40
38
|
|
41
39
|
def authenticate_session(request: Request):
|
42
40
|
session_id = request.cookies.get("session_id")
|
43
|
-
if session_id is None
|
41
|
+
if session_id is None:
|
44
42
|
raise HTTPException(
|
45
43
|
status_code=302,
|
46
44
|
headers={"location": str(request.url_for("login"))},
|
47
45
|
detail="Invalid session ID",
|
48
46
|
)
|
49
47
|
user = get_user_from_session(session_id)
|
48
|
+
if user is None:
|
49
|
+
raise HTTPException(
|
50
|
+
status_code=302,
|
51
|
+
headers={"location": str(request.url_for("login"))},
|
52
|
+
detail="Invalid username",
|
53
|
+
)
|
50
54
|
return user
|
51
55
|
|
52
56
|
|
53
57
|
def authenticate_api_session(request: Request):
|
54
58
|
session_id = request.cookies.get("session_id")
|
55
|
-
if session_id is None or session_id not in sessions:
|
56
|
-
raise HTTPException(status_code=401, detail="Not logged in")
|
57
59
|
user = get_user_from_session(session_id)
|
60
|
+
if user is None:
|
61
|
+
raise HTTPException(status_code=401, detail="Not logged in")
|
58
62
|
return user
|
59
63
|
|
60
64
|
|
61
65
|
def authenticate_ws_session(websocket: WebSocket):
|
62
66
|
session_id = websocket.cookies.get("session_id")
|
63
|
-
if session_id is None or session_id not in sessions:
|
64
|
-
raise HTTPException(status_code=401, detail="Not logged in")
|
65
67
|
user = get_user_from_session(session_id)
|
68
|
+
if user is None:
|
69
|
+
raise HTTPException(status_code=401, detail="Not logged in")
|
66
70
|
return user
|
67
71
|
|
68
72
|
|
69
73
|
def auto_redirect(request: Request):
|
70
74
|
session_id = request.cookies.get("session_id")
|
71
|
-
if session_id is None
|
75
|
+
if get_user_from_session(session_id) is None:
|
72
76
|
return request
|
73
77
|
raise HTTPException(
|
74
78
|
status_code=302,
|
@@ -78,16 +82,20 @@ def auto_redirect(request: Request):
|
|
78
82
|
|
79
83
|
|
80
84
|
def get_user_from_session(session_id: str):
|
81
|
-
|
82
|
-
|
83
|
-
|
85
|
+
if session_id is None:
|
86
|
+
return
|
87
|
+
try:
|
88
|
+
session_data = jwt.decode(session_id, SECRET)
|
89
|
+
return session_data.claims["username"]
|
90
|
+
except (BadSignatureError, LookupError):
|
91
|
+
pass
|
84
92
|
|
85
93
|
|
86
94
|
def get_current_user(request: Request):
|
87
95
|
session_id = request.cookies.get("session_id")
|
88
|
-
if session_id is None or session_id not in sessions:
|
89
|
-
return None
|
90
96
|
user = get_user_from_session(session_id)
|
97
|
+
if user is None:
|
98
|
+
return None
|
91
99
|
return USERS[user]
|
92
100
|
|
93
101
|
|
goosebit/models.py
CHANGED
@@ -2,16 +2,21 @@ from tortoise import Model, fields
|
|
2
2
|
|
3
3
|
|
4
4
|
class Tag(Model):
|
5
|
-
id = fields.IntField(
|
5
|
+
id = fields.IntField(primary_key=True)
|
6
6
|
name = fields.CharField(max_length=255)
|
7
7
|
|
8
8
|
|
9
9
|
class Device(Model):
|
10
|
-
uuid = fields.CharField(max_length=255,
|
10
|
+
uuid = fields.CharField(max_length=255, primary_key=True)
|
11
11
|
name = fields.CharField(max_length=255, null=True)
|
12
12
|
fw_file = fields.CharField(max_length=255, default="latest")
|
13
13
|
fw_version = fields.CharField(max_length=255, null=True)
|
14
|
-
|
14
|
+
hw_model = fields.CharField(max_length=255, null=True, default="default")
|
15
|
+
hw_revision = fields.CharField(max_length=255, null=True, default="default")
|
16
|
+
feed = fields.CharField(max_length=255, default="default")
|
17
|
+
flavor = fields.CharField(max_length=255, default="default")
|
18
|
+
last_state = fields.CharField(max_length=255, null=True, default="unknown")
|
19
|
+
progress = fields.IntField(null=True)
|
15
20
|
last_log = fields.TextField(null=True)
|
16
21
|
last_seen = fields.BigIntField(null=True)
|
17
22
|
last_ip = fields.CharField(max_length=15, null=True)
|
@@ -19,3 +24,17 @@ class Device(Model):
|
|
19
24
|
tags = fields.ManyToManyField(
|
20
25
|
"models.Tag", related_name="devices", through="device_tags"
|
21
26
|
)
|
27
|
+
|
28
|
+
|
29
|
+
class Rollout(Model):
|
30
|
+
id = fields.IntField(primary_key=True)
|
31
|
+
created_at = fields.DatetimeField(auto_now_add=True)
|
32
|
+
name = fields.CharField(max_length=255, null=True)
|
33
|
+
hw_model = fields.CharField(max_length=255, default="default")
|
34
|
+
hw_revision = fields.CharField(max_length=255, default="default")
|
35
|
+
feed = fields.CharField(max_length=255, default="default")
|
36
|
+
flavor = fields.CharField(max_length=255, default="default")
|
37
|
+
fw_file = fields.CharField(max_length=255)
|
38
|
+
paused = fields.BooleanField(default=False)
|
39
|
+
success_count = fields.IntField(default=0)
|
40
|
+
failure_count = fields.IntField(default=0)
|
goosebit/permissions.py
CHANGED
@@ -22,10 +22,10 @@ class DevicePermissions(PermissionsBase):
|
|
22
22
|
DELETE = "devices.delete"
|
23
23
|
|
24
24
|
|
25
|
-
class
|
26
|
-
READ = "
|
27
|
-
WRITE = "
|
28
|
-
DELETE = "
|
25
|
+
class RolloutPermissions(PermissionsBase):
|
26
|
+
READ = "rollouts.read"
|
27
|
+
WRITE = "rollouts.write"
|
28
|
+
DELETE = "rollouts.delete"
|
29
29
|
|
30
30
|
|
31
31
|
class HomePermissions(PermissionsBase):
|
@@ -36,15 +36,29 @@ class Permissions:
|
|
36
36
|
HOME = HomePermissions
|
37
37
|
FIRMWARE = FirmwarePermissions
|
38
38
|
DEVICE = DevicePermissions
|
39
|
-
|
39
|
+
ROLLOUT = RolloutPermissions
|
40
40
|
|
41
41
|
@classmethod
|
42
42
|
def full(cls):
|
43
43
|
all_items = set()
|
44
|
-
for item in [cls.HOME, cls.FIRMWARE, cls.DEVICE, cls.
|
44
|
+
for item in [cls.HOME, cls.FIRMWARE, cls.DEVICE, cls.ROLLOUT]:
|
45
45
|
all_items.update(item.full())
|
46
46
|
return list(all_items)
|
47
47
|
|
48
|
+
@classmethod
|
49
|
+
def from_str(cls, permission: str):
|
50
|
+
if permission == "*":
|
51
|
+
return cls.full()
|
52
|
+
permission_type = permission.split(".")[0]
|
53
|
+
if permission_type == "firmware":
|
54
|
+
return FirmwarePermissions(permission)
|
55
|
+
if permission_type == "devices":
|
56
|
+
return DevicePermissions(permission)
|
57
|
+
if permission_type == "rollouts":
|
58
|
+
return RolloutPermissions(permission)
|
59
|
+
if permission_type == "home":
|
60
|
+
return HomePermissions(permission)
|
61
|
+
|
48
62
|
|
49
63
|
ADMIN = Permissions.full()
|
50
64
|
MONITORING = [
|
goosebit/realtime/logs.py
CHANGED
@@ -14,6 +14,7 @@ router = APIRouter(prefix="/logs")
|
|
14
14
|
|
15
15
|
class RealtimeLogModel(BaseModel):
|
16
16
|
log: str | None
|
17
|
+
progress: int | None
|
17
18
|
clear: bool = False
|
18
19
|
|
19
20
|
|
@@ -29,7 +30,7 @@ async def device_logs(websocket: WebSocket, dev_id: str):
|
|
29
30
|
manager = await get_update_manager(dev_id)
|
30
31
|
|
31
32
|
async def callback(log_update):
|
32
|
-
data = RealtimeLogModel(log=log_update)
|
33
|
+
data = RealtimeLogModel(log=log_update, progress=manager.device.progress)
|
33
34
|
if log_update is None:
|
34
35
|
data.clear = True
|
35
36
|
data.log = ""
|
goosebit/settings.py
CHANGED
@@ -1,55 +1,66 @@
|
|
1
|
+
import secrets
|
1
2
|
from dataclasses import dataclass
|
2
3
|
from pathlib import Path
|
3
4
|
|
4
|
-
|
5
|
+
import yaml
|
6
|
+
from argon2 import PasswordHasher
|
5
7
|
|
6
|
-
from goosebit import
|
8
|
+
from goosebit.permissions import Permissions
|
9
|
+
from goosebit.updates.version import UpdateVersionParser
|
7
10
|
|
8
|
-
|
9
|
-
POLL_MINUTES = 1
|
10
|
-
POLL_HOURS = 0
|
11
|
-
POLL_TIME = ":".join(
|
12
|
-
[
|
13
|
-
str(POLL_HOURS).rjust(2, "0"),
|
14
|
-
str(POLL_MINUTES).rjust(2, "0"),
|
15
|
-
str(POLL_SECONDS).rjust(2, "0"),
|
16
|
-
]
|
17
|
-
)
|
18
|
-
|
19
|
-
BASE_DIR = Path(__file__).resolve().parent
|
11
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
20
12
|
TOKEN_SWU_DIR = BASE_DIR.joinpath("swugen")
|
21
13
|
SWUPDATE_FILES_DIR = BASE_DIR.joinpath("swupdate")
|
22
14
|
UPDATES_DIR = BASE_DIR.joinpath("updates")
|
23
|
-
DB_LOC = BASE_DIR.joinpath("db.sqlite3")
|
24
15
|
DB_MIGRATIONS_LOC = BASE_DIR.joinpath("migrations")
|
25
|
-
DB_URI = f"sqlite:///{DB_LOC}"
|
26
16
|
|
27
|
-
SECRET =
|
28
|
-
PWD_CXT =
|
17
|
+
SECRET = secrets.token_hex(16)
|
18
|
+
PWD_CXT = PasswordHasher()
|
19
|
+
|
20
|
+
with open(BASE_DIR.joinpath("settings.yaml"), "r") as f:
|
21
|
+
config = yaml.safe_load(f.read())
|
29
22
|
|
30
|
-
|
23
|
+
TENANT = config.get("tenant", "DEFAULT")
|
24
|
+
|
25
|
+
POLL_TIME = config.get("poll_time_default", "00:01:00")
|
26
|
+
POLL_TIME_UPDATING = config.get("poll_time_updating", "00:00:05")
|
27
|
+
POLL_TIME_REGISTRATION = config.get("poll_time_registration", "00:00:10")
|
28
|
+
|
29
|
+
DB_LOC = BASE_DIR.joinpath(config.get("db_location", "db.sqlite3"))
|
30
|
+
DB_URI = f"sqlite:///{DB_LOC}"
|
31
|
+
|
32
|
+
UPDATE_VERSION_PARSER = UpdateVersionParser.create(
|
33
|
+
parse_mode=config.get("version_format", "datetime"),
|
34
|
+
)
|
31
35
|
|
32
36
|
|
33
37
|
@dataclass
|
34
38
|
class User:
|
35
39
|
username: str
|
36
40
|
hashed_pwd: str
|
37
|
-
permissions:
|
41
|
+
permissions: set
|
38
42
|
|
39
43
|
def get_json_permissions(self):
|
40
44
|
return [str(p) for p in self.permissions]
|
41
45
|
|
42
46
|
|
43
|
-
|
44
|
-
|
47
|
+
users: dict[str, User] = {}
|
48
|
+
|
45
49
|
|
50
|
+
def add_user(u: User):
|
51
|
+
users[u.username] = u
|
46
52
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
+
|
54
|
+
for user in config.get("users", []):
|
55
|
+
permissions = set()
|
56
|
+
for p in user["permissions"]:
|
57
|
+
permissions.update(Permissions.from_str(p))
|
58
|
+
add_user(
|
59
|
+
User(
|
60
|
+
username=user["email"],
|
61
|
+
hashed_pwd=PWD_CXT.hash(user["password"]),
|
62
|
+
permissions=permissions,
|
63
|
+
)
|
53
64
|
)
|
54
|
-
|
65
|
+
|
55
66
|
USERS = users
|
goosebit/ui/routes.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import aiofiles
|
2
|
-
from fastapi import APIRouter, Depends, Form, Security, UploadFile
|
2
|
+
from fastapi import APIRouter, Depends, Form, HTTPException, Security, UploadFile
|
3
3
|
from fastapi.requests import Request
|
4
4
|
from fastapi.responses import RedirectResponse
|
5
5
|
from fastapi.security import OAuth2PasswordBearer
|
@@ -8,6 +8,7 @@ from goosebit.auth import authenticate_session, validate_user_permissions
|
|
8
8
|
from goosebit.permissions import Permissions
|
9
9
|
from goosebit.settings import UPDATES_DIR
|
10
10
|
from goosebit.ui.templates import templates
|
11
|
+
from goosebit.updater.misc import validate_filename
|
11
12
|
|
12
13
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
13
14
|
|
@@ -46,6 +47,9 @@ async def upload_update(
|
|
46
47
|
done: bool = Form(...),
|
47
48
|
filename: str = Form(...),
|
48
49
|
):
|
50
|
+
if not validate_filename(filename):
|
51
|
+
raise HTTPException(400, detail="Could not parse file data, invalid filename.")
|
52
|
+
|
49
53
|
file = UPDATES_DIR.joinpath(filename)
|
50
54
|
tmpfile = file.with_suffix(".tmp")
|
51
55
|
contents = await chunk.read()
|
@@ -81,24 +85,24 @@ async def devices_ui(request: Request):
|
|
81
85
|
|
82
86
|
|
83
87
|
@router.get(
|
84
|
-
"/
|
88
|
+
"/rollouts",
|
85
89
|
dependencies=[
|
86
|
-
Security(validate_user_permissions, scopes=[Permissions.
|
90
|
+
Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])
|
87
91
|
],
|
88
92
|
)
|
89
|
-
async def
|
93
|
+
async def rollouts_ui(request: Request):
|
90
94
|
return templates.TemplateResponse(
|
91
|
-
"
|
95
|
+
"rollouts.html", context={"request": request, "title": "Rollouts"}
|
92
96
|
)
|
93
97
|
|
94
98
|
|
95
99
|
@router.get(
|
96
|
-
"/
|
100
|
+
"/logs/{dev_id}",
|
97
101
|
dependencies=[
|
98
|
-
Security(validate_user_permissions, scopes=[Permissions.
|
102
|
+
Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
|
99
103
|
],
|
100
104
|
)
|
101
|
-
async def
|
105
|
+
async def logs_ui(request: Request, dev_id: str):
|
102
106
|
return templates.TemplateResponse(
|
103
|
-
"
|
107
|
+
"logs.html", context={"request": request, "title": "Log", "device": dev_id}
|
104
108
|
)
|
goosebit/ui/static/js/devices.js
CHANGED
@@ -40,6 +40,8 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
40
40
|
}
|
41
41
|
},
|
42
42
|
{ data: 'uuid' },
|
43
|
+
{ data: 'hw_model' },
|
44
|
+
{ data: 'hw_revision' },
|
43
45
|
{ data: 'fw' },
|
44
46
|
{
|
45
47
|
data: 'force_update',
|
@@ -56,6 +58,16 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
56
58
|
}
|
57
59
|
},
|
58
60
|
{ data: 'fw_file' },
|
61
|
+
{
|
62
|
+
data: 'progress',
|
63
|
+
render: function(data, type, row) {
|
64
|
+
if ( type === 'display' || type === 'filter' ) {
|
65
|
+
return (data || "❓") + "%";
|
66
|
+
}
|
67
|
+
return data;
|
68
|
+
}
|
69
|
+
|
70
|
+
},
|
59
71
|
{ data: 'last_ip' },
|
60
72
|
{
|
61
73
|
data: 'last_seen',
|
@@ -210,17 +222,20 @@ function updateFirmwareSelection() {
|
|
210
222
|
.then(data => {
|
211
223
|
selectElem = document.getElementById("device-selected-fw");
|
212
224
|
|
225
|
+
optionElem = document.createElement("option");
|
226
|
+
optionElem.value = "none";
|
227
|
+
optionElem.textContent = "none";
|
228
|
+
selectElem.appendChild(optionElem);
|
229
|
+
|
213
230
|
optionElem = document.createElement("option");
|
214
231
|
optionElem.value = "latest";
|
215
232
|
optionElem.textContent = "latest";
|
216
|
-
|
217
233
|
selectElem.appendChild(optionElem);
|
218
234
|
|
219
235
|
data.forEach(item => {
|
220
236
|
optionElem = document.createElement("option");
|
221
237
|
optionElem.value = item["name"];
|
222
238
|
optionElem.textContent = item["name"];
|
223
|
-
|
224
239
|
selectElem.appendChild(optionElem);
|
225
240
|
});
|
226
241
|
})
|
@@ -45,6 +45,15 @@ const sendFileChunks = async (file) => {
|
|
45
45
|
const progress = (uploadedChunks / totalChunks) * 100;
|
46
46
|
progressBar.style.width = `${progress}%`;
|
47
47
|
progressBar.innerHTML = `${Math.round(progress)}%`;
|
48
|
+
} else {
|
49
|
+
if (response.status === 400) {
|
50
|
+
result = await response.json()
|
51
|
+
alerts = document.getElementById("upload-alerts");
|
52
|
+
alerts.innerHTML = `<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
53
|
+
${result["detail"]}
|
54
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
55
|
+
</div>`
|
56
|
+
}
|
48
57
|
}
|
49
58
|
|
50
59
|
start = end;
|
@@ -84,7 +93,7 @@ function updateFirmwareList() {
|
|
84
93
|
|
85
94
|
data.forEach(item => {
|
86
95
|
const listItem = document.createElement('li');
|
87
|
-
listItem.textContent = item["
|
96
|
+
listItem.textContent = `${item["name"]}, size: ${(item["size"] / 1024 / 1024).toFixed(2)} MB`;
|
88
97
|
listItem.classList = ["list-group-item d-flex justify-content-between align-items-center"];
|
89
98
|
|
90
99
|
const btnGroup = document.createElement("div")
|
goosebit/ui/static/js/index.js
CHANGED
@@ -40,6 +40,16 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
40
40
|
},
|
41
41
|
{ data: 'uuid' },
|
42
42
|
{ data: 'fw' },
|
43
|
+
{
|
44
|
+
data: 'progress',
|
45
|
+
render: function(data, type, row) {
|
46
|
+
if ( type === 'display' || type === 'filter' ) {
|
47
|
+
return (data || "❓") + "%";
|
48
|
+
}
|
49
|
+
return data;
|
50
|
+
}
|
51
|
+
|
52
|
+
},
|
43
53
|
{ data: 'last_ip' },
|
44
54
|
{
|
45
55
|
data: 'last_seen',
|
@@ -125,7 +135,7 @@ function updateBtnState() {
|
|
125
135
|
|
126
136
|
function downloadLogins(devices) {
|
127
137
|
let deviceLogins = devices.map(dev => {
|
128
|
-
return [dev["name"], `https://${dev["uuid"]}-access.loadsync.io`, dev["uuid"]
|
138
|
+
return [dev["name"], `https://${dev["uuid"]}-access.loadsync.io`, dev["uuid"]];
|
129
139
|
});
|
130
140
|
deviceLogins.unshift(["Building", "Access Link", "Serial Number/Wifi SSID", "Login/Wifi Password"]);
|
131
141
|
|
goosebit/ui/static/js/logs.js
CHANGED
@@ -9,6 +9,10 @@ document.addEventListener("DOMContentLoaded", function() {
|
|
9
9
|
logElem.textContent = ""
|
10
10
|
}
|
11
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
|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function() {
|
2
|
+
var dataTable = new DataTable("#rollout-table", {
|
3
|
+
responsive: true,
|
4
|
+
paging: false,
|
5
|
+
scrollCollapse: true,
|
6
|
+
scroller: true,
|
7
|
+
scrollY: "65vh",
|
8
|
+
stateSave: true,
|
9
|
+
select: true,
|
10
|
+
rowId: "id",
|
11
|
+
ajax: {
|
12
|
+
url: "/api/rollouts/all",
|
13
|
+
dataSrc: "",
|
14
|
+
},
|
15
|
+
initComplete:function(){
|
16
|
+
},
|
17
|
+
columnDefs: [],
|
18
|
+
columns: [
|
19
|
+
{ data: 'id' },
|
20
|
+
{ data: 'created_at' },
|
21
|
+
{ data: 'name' },
|
22
|
+
{ data: 'hw_model' },
|
23
|
+
{ data: 'hw_revision' },
|
24
|
+
{ data: 'feed' },
|
25
|
+
{ data: 'flavor' },
|
26
|
+
{ data: 'fw_file' },
|
27
|
+
{ data: 'paused',
|
28
|
+
render: function(data, type) {
|
29
|
+
if ( type === 'display' || type === 'filter' ) {
|
30
|
+
color = data ? "success" : "light"
|
31
|
+
return `
|
32
|
+
<div class="text-${color}">
|
33
|
+
●
|
34
|
+
</div>
|
35
|
+
`
|
36
|
+
}
|
37
|
+
return data;
|
38
|
+
}
|
39
|
+
},
|
40
|
+
{ data: 'success_count' },
|
41
|
+
{ data: 'failure_count' },
|
42
|
+
],
|
43
|
+
layout: {
|
44
|
+
top1Start: {
|
45
|
+
buttons: [
|
46
|
+
]
|
47
|
+
},
|
48
|
+
bottom1Start: {
|
49
|
+
buttons: [
|
50
|
+
]
|
51
|
+
}
|
52
|
+
},
|
53
|
+
});
|
54
|
+
|
55
|
+
dataTable.ajax.reload();
|
56
|
+
});
|