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 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("/ui/home", status_code=302)
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("/login", status_code=302)
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": last_seen < 120 if last_seen is not None else None,
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, get_newest_fw
8
- from goosebit.updater.updates import FirmwareArtifact
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=[
@@ -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 jose import jwt
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(password, user.hashed_pwd):
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
- sessions = {}
33
-
34
-
35
- def create_session(email: str) -> str:
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 or session_id not in sessions:
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 or session_id not in sessions:
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
- for username in USERS:
82
- if username == sessions.get(session_id):
83
- return username
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(pk=True)
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, pk=True)
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
- last_state = fields.CharField(max_length=255, null=True)
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 TunnelPermissions(PermissionsBase):
26
- READ = "tunnels.read"
27
- WRITE = "tunnels.write"
28
- DELETE = "tunnels.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
- TUNNEL = TunnelPermissions
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.TUNNEL]:
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
- from passlib.context import CryptContext
5
+ import yaml
6
+ from argon2 import PasswordHasher
5
7
 
6
- from goosebit import permissions
8
+ from goosebit.permissions import Permissions
9
+ from goosebit.updates.version import UpdateVersionParser
7
10
 
8
- POLL_SECONDS = 0
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 = "123456789"
28
- PWD_CXT = CryptContext(schemes=["bcrypt"], deprecated="auto")
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
- users = {}
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: list
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
- def add_user(user: User):
44
- users[user.username] = user
47
+ users: dict[str, User] = {}
48
+
45
49
 
50
+ def add_user(u: User):
51
+ users[u.username] = u
46
52
 
47
- # User configuration
48
- add_user(
49
- User(
50
- username="admin@goosebit.local",
51
- hashed_pwd=PWD_CXT.hash("admin"),
52
- permissions=permissions.ADMIN,
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
- "/logs/{dev_id}",
88
+ "/rollouts",
85
89
  dependencies=[
86
- Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
90
+ Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])
87
91
  ],
88
92
  )
89
- async def logs_ui(request: Request, dev_id: str):
93
+ async def rollouts_ui(request: Request):
90
94
  return templates.TemplateResponse(
91
- "logs.html", context={"request": request, "title": "Log", "device": dev_id}
95
+ "rollouts.html", context={"request": request, "title": "Rollouts"}
92
96
  )
93
97
 
94
98
 
95
99
  @router.get(
96
- "/tunnels",
100
+ "/logs/{dev_id}",
97
101
  dependencies=[
98
- Security(validate_user_permissions, scopes=[Permissions.TUNNEL.READ])
102
+ Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
99
103
  ],
100
104
  )
101
- async def tunnels_ui(request: Request):
105
+ async def logs_ui(request: Request, dev_id: str):
102
106
  return templates.TemplateResponse(
103
- "tunnels.html", context={"request": request, "title": "Tunnels"}
107
+ "logs.html", context={"request": request, "title": "Log", "device": dev_id}
104
108
  )
@@ -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["version"];
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")
@@ -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"], dev["web_pwd"]];
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
 
@@ -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
+ });