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.
Files changed (52) hide show
  1. goosebit/__init__.py +8 -5
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +60 -36
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +37 -44
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +87 -0
  8. goosebit/api/routes.py +15 -7
  9. goosebit/auth/__init__.py +37 -21
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +125 -6
  12. goosebit/permissions.py +33 -13
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +4 -6
  15. goosebit/settings.py +38 -29
  16. goosebit/telemetry/__init__.py +28 -0
  17. goosebit/telemetry/prometheus.py +10 -0
  18. goosebit/ui/__init__.py +1 -1
  19. goosebit/ui/routes.py +36 -39
  20. goosebit/ui/static/js/devices.js +191 -239
  21. goosebit/ui/static/js/firmware.js +234 -88
  22. goosebit/ui/static/js/index.js +83 -84
  23. goosebit/ui/static/js/logs.js +17 -10
  24. goosebit/ui/static/js/rollouts.js +198 -0
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -42
  27. goosebit/ui/templates/firmware.html +150 -34
  28. goosebit/ui/templates/index.html +9 -23
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +18 -3
  31. goosebit/ui/templates/nav.html +78 -25
  32. goosebit/ui/templates/rollouts.html +76 -0
  33. goosebit/updater/__init__.py +1 -1
  34. goosebit/updater/controller/__init__.py +1 -1
  35. goosebit/updater/controller/v1/__init__.py +1 -1
  36. goosebit/updater/controller/v1/routes.py +112 -24
  37. goosebit/updater/manager.py +237 -94
  38. goosebit/updater/routes.py +7 -8
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. goosebit-0.1.2.dist-info/METADATA +123 -0
  42. goosebit-0.1.2.dist-info/RECORD +51 -0
  43. goosebit/updater/download/__init__.py +0 -1
  44. goosebit/updater/download/routes.py +0 -6
  45. goosebit/updater/download/v1/__init__.py +0 -1
  46. goosebit/updater/download/v1/routes.py +0 -26
  47. goosebit/updater/misc.py +0 -69
  48. goosebit/updater/updates.py +0 -93
  49. goosebit-0.1.0.dist-info/METADATA +0 -37
  50. goosebit-0.1.0.dist-info/RECORD +0 -48
  51. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,198 @@
1
+ let dataTable;
2
+
3
+ document.addEventListener("DOMContentLoaded", async () => {
4
+ dataTable = new DataTable("#rollout-table", {
5
+ responsive: true,
6
+ paging: true,
7
+ processing: true,
8
+ serverSide: true,
9
+ order: [1, "desc"],
10
+ scrollCollapse: true,
11
+ scroller: true,
12
+ scrollY: "65vh",
13
+ stateSave: true,
14
+ stateLoadParams: (settings, data) => {
15
+ // if save state is older than last breaking code change...
16
+ if (data.time <= 1722413708000) {
17
+ // ... delete it
18
+ for (const key of Object.keys(data)) {
19
+ delete data[key];
20
+ }
21
+ }
22
+ },
23
+ select: true,
24
+ rowId: "id",
25
+ ajax: {
26
+ url: "/api/rollouts/all",
27
+ contentType: "application/json",
28
+ },
29
+ initComplete: () => {
30
+ updateBtnState();
31
+ },
32
+ columnDefs: [
33
+ {
34
+ targets: [1, 2, 3, 4],
35
+ searchable: true,
36
+ orderable: true,
37
+ },
38
+ {
39
+ targets: "_all",
40
+ searchable: false,
41
+ orderable: false,
42
+ },
43
+ ],
44
+ columns: [
45
+ { data: "id", visible: false },
46
+ { data: "created_at", render: (data) => new Date(data).toLocaleString() },
47
+ { data: "name" },
48
+ { data: "feed" },
49
+ { data: "flavor" },
50
+ { data: "fw_file" },
51
+ { data: "fw_version" },
52
+ {
53
+ data: "paused",
54
+ render: (data, type) => {
55
+ if (type === "display" || type === "filter") {
56
+ const color = data ? "danger" : "muted";
57
+ return `
58
+ <div class="text-${color}">
59
+
60
+ </div>
61
+ `;
62
+ }
63
+ return data;
64
+ },
65
+ },
66
+ { data: "success_count" },
67
+ { data: "failure_count" },
68
+ ],
69
+ layout: {
70
+ top1Start: {
71
+ buttons: [],
72
+ },
73
+ bottom1Start: {
74
+ buttons: [
75
+ {
76
+ text: '<i class="bi bi-plus" ></i>',
77
+ action: () => {
78
+ new bootstrap.Modal("#rollout-create-modal").show();
79
+ },
80
+ className: "buttons-create",
81
+ titleAttr: "Create Rollout",
82
+ },
83
+ {
84
+ text: '<i class="bi bi-play-fill" ></i>',
85
+ action: (e, dt) => {
86
+ const selectedRollouts = dt
87
+ .rows({ selected: true })
88
+ .data()
89
+ .toArray()
90
+ .map((d) => d.id);
91
+ pauseRollouts(selectedRollouts, false);
92
+ },
93
+ className: "buttons-resume",
94
+ titleAttr: "Resume Rollouts",
95
+ },
96
+ {
97
+ text: '<i class="bi bi-pause-fill" ></i>',
98
+ action: (e, dt) => {
99
+ const selectedRollouts = dt
100
+ .rows({ selected: true })
101
+ .data()
102
+ .toArray()
103
+ .map((d) => d.id);
104
+ pauseRollouts(selectedRollouts, true);
105
+ },
106
+ className: "buttons-pause",
107
+ titleAttr: "Pause Rollouts",
108
+ },
109
+ {
110
+ text: '<i class="bi bi-trash" ></i>',
111
+ action: (e, dt) => {
112
+ const selectedRollouts = dt
113
+ .rows({ selected: true })
114
+ .data()
115
+ .toArray()
116
+ .map((d) => d.id);
117
+ deleteRollouts(selectedRollouts);
118
+ },
119
+ className: "buttons-delete",
120
+ titleAttr: "Delete Rollouts",
121
+ },
122
+ ],
123
+ },
124
+ },
125
+ });
126
+
127
+ dataTable
128
+ .on("select", () => {
129
+ updateBtnState();
130
+ })
131
+ .on("deselect", () => {
132
+ updateBtnState();
133
+ });
134
+
135
+ updateRolloutList();
136
+
137
+ await updateFirmwareSelection();
138
+ });
139
+
140
+ function updateBtnState() {
141
+ if (dataTable.rows({ selected: true }).any()) {
142
+ document.querySelector("button.buttons-delete").classList.remove("disabled");
143
+ } else {
144
+ document.querySelector("button.buttons-delete").classList.add("disabled");
145
+ }
146
+
147
+ if (dataTable.rows((_, data) => data.paused, { selected: true }).any()) {
148
+ document.querySelector("button.buttons-resume").classList.remove("disabled");
149
+ } else {
150
+ document.querySelector("button.buttons-resume").classList.add("disabled");
151
+ }
152
+
153
+ if (dataTable.rows((_, data) => !data.paused, { selected: true }).any()) {
154
+ document.querySelector("button.buttons-pause").classList.remove("disabled");
155
+ } else {
156
+ document.querySelector("button.buttons-pause").classList.add("disabled");
157
+ }
158
+ }
159
+
160
+ async function createRollout() {
161
+ const name = document.getElementById("rollout-selected-name").value;
162
+ const feed = document.getElementById("rollout-selected-feed").value;
163
+ const flavor = document.getElementById("rollout-selected-flavor").value;
164
+ const firmware_id = document.getElementById("selected-fw").value;
165
+
166
+ try {
167
+ await post("/api/rollouts", { name, feed, flavor, firmware_id });
168
+ } catch (error) {
169
+ console.error("Rollout creation failed:", error);
170
+ }
171
+
172
+ setTimeout(updateRolloutList, 50);
173
+ }
174
+
175
+ function updateRolloutList() {
176
+ dataTable.ajax.reload();
177
+ }
178
+
179
+ async function deleteRollouts(ids) {
180
+ try {
181
+ await post("/api/rollouts/delete", { ids });
182
+ } catch (error) {
183
+ console.error("Rollouts deletion failed:", error);
184
+ }
185
+
186
+ updateBtnState();
187
+ setTimeout(updateRolloutList, 50);
188
+ }
189
+
190
+ async function pauseRollouts(ids, paused) {
191
+ try {
192
+ await post("/api/rollouts/update", { ids, paused });
193
+ } catch (error) {
194
+ console.error(`Rollouts ${paused ? "pausing" : "unpausing"} failed:`, error);
195
+ }
196
+
197
+ setTimeout(updateRolloutList, 50);
198
+ }
@@ -0,0 +1,66 @@
1
+ function secondsToRecentDate(t) {
2
+ if (t == null) {
3
+ return null;
4
+ }
5
+ const time = Number(t);
6
+ const d = Math.floor(time / 86400);
7
+ const h = Math.floor((time % 86400) / 3600);
8
+ const m = Math.floor(((time % 86400) % 3600) / 60);
9
+ const s = Math.floor(((time % 86400) % 3600) % 60);
10
+
11
+ if (d > 0) {
12
+ return d + (d === 1 ? " day" : " days");
13
+ }
14
+ if (h > 0) {
15
+ return h + (h === 1 ? " hour" : " hours");
16
+ }
17
+ if (m > 0) {
18
+ return m + (m === 1 ? " minute" : " minutes");
19
+ }
20
+ return s + (s === 1 ? " second" : " seconds");
21
+ }
22
+
23
+ async function updateFirmwareSelection(addSpecialMode = false) {
24
+ try {
25
+ const response = await fetch("/api/firmware/all");
26
+ if (!response.ok) {
27
+ console.error("Retrieving firmwares failed.");
28
+ return;
29
+ }
30
+ const data = (await response.json()).data;
31
+ const selectElem = document.getElementById("selected-fw");
32
+
33
+ if (addSpecialMode) {
34
+ let optionElem = document.createElement("option");
35
+ optionElem.value = "rollout";
36
+ optionElem.textContent = "Rollout";
37
+ selectElem.appendChild(optionElem);
38
+
39
+ optionElem = document.createElement("option");
40
+ optionElem.value = "latest";
41
+ optionElem.textContent = "Latest";
42
+ selectElem.appendChild(optionElem);
43
+ }
44
+
45
+ for (const item of data) {
46
+ const optionElem = document.createElement("option");
47
+ optionElem.value = item.id;
48
+ optionElem.textContent = item.name;
49
+ selectElem.appendChild(optionElem);
50
+ }
51
+ } catch (error) {
52
+ console.error("Failed to fetch device data:", error);
53
+ }
54
+ }
55
+
56
+ async function post(url, object) {
57
+ const response = await fetch(url, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/json" },
60
+ body: JSON.stringify(object),
61
+ });
62
+
63
+ if (!response.ok) {
64
+ throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
65
+ }
66
+ }
@@ -1,43 +1,28 @@
1
- {% extends "nav.html" %}
2
- {% block content %}
1
+ {% extends "nav.html" %} {% block content %}
3
2
  <div class="container-fluid">
4
3
  <div class="row p-2 d-flex justify-content-center">
5
4
  <div class="col">
6
5
  <table id="device-table" class="table table-hover">
7
6
  <thead>
8
7
  <tr>
9
- <th>
10
- Name
11
- </th>
12
- <th>
13
- Up
14
- </th>
15
- <th>
16
- UUID
17
- </th>
18
- <th>
19
- Firmware
20
- </th>
21
- <th>
22
- Force Update
23
- </th>
24
- <th>
25
- Update File
26
- </th>
27
- <th>
28
- Last IP
29
- </th>
30
- <th>
31
- Last Seen
32
- </th>
33
- <th>
34
- State
35
- </th>
8
+ <th>Up</th>
9
+ <th>UUID</th>
10
+ <th>Name</th>
11
+ <th>Model</th>
12
+ <th>Revision</th>
13
+ <th>Feed</th>
14
+ <th>Flavor</th>
15
+ <th>Installed Firmware</th>
16
+ <th>Target Firmware</th>
17
+ <th>Update Mode</th>
18
+ <th>State</th>
19
+ <th>Force Update</th>
20
+ <th>Progress</th>
21
+ <th>Last IP</th>
22
+ <th>Last Seen</th>
36
23
  </tr>
37
24
  </thead>
38
- <tbody id="devices-list">
39
-
40
- </tbody>
25
+ <tbody id="devices-list"></tbody>
41
26
  </table>
42
27
  </div>
43
28
  </div>
@@ -47,16 +32,42 @@
47
32
  <div class="modal-content">
48
33
  <div class="modal-header">
49
34
  <h5 class="modal-title">Configure Devices</h5>
50
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
35
+ <button
36
+ type="button"
37
+ class="btn-close"
38
+ data-bs-dismiss="modal"
39
+ aria-label="Close"
40
+ ></button>
51
41
  </div>
52
42
  <div class="modal-body">
53
- <select class="form-select" id="device-selected-fw">
54
-
55
- </select>
43
+ <input
44
+ id="rollout-selected-feed"
45
+ class="form-control mb-3"
46
+ placeholder="Feed"
47
+ />
48
+ <input
49
+ id="rollout-selected-flavor"
50
+ class="form-control mb-3"
51
+ placeholder="Flavor"
52
+ />
53
+ <select class="form-select" id="selected-fw"></select>
56
54
  </div>
57
55
  <div class="modal-footer">
58
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
59
- <button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="updateDeviceConfig()">Save changes</button>
56
+ <button
57
+ type="button"
58
+ class="btn btn-secondary"
59
+ data-bs-dismiss="modal"
60
+ >
61
+ Close
62
+ </button>
63
+ <button
64
+ type="button"
65
+ class="btn btn-outline-light"
66
+ data-bs-dismiss="modal"
67
+ onclick="updateDeviceConfig()"
68
+ >
69
+ Save changes
70
+ </button>
60
71
  </div>
61
72
  </div>
62
73
  </div>
@@ -66,14 +77,36 @@
66
77
  <div class="modal-content">
67
78
  <div class="modal-header">
68
79
  <h5 class="modal-title">Rename Device</h5>
69
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
80
+ <button
81
+ type="button"
82
+ class="btn-close"
83
+ data-bs-dismiss="modal"
84
+ aria-label="Close"
85
+ ></button>
70
86
  </div>
71
87
  <div class="modal-body">
72
- <input id="device-selected-name" class="form-control" placeholder="Name"/>
88
+ <input
89
+ id="device-selected-name"
90
+ class="form-control"
91
+ placeholder="Name"
92
+ />
73
93
  </div>
74
94
  <div class="modal-footer">
75
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
76
- <button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="updateDeviceName()">Save changes</button>
95
+ <button
96
+ type="button"
97
+ class="btn btn-secondary"
98
+ data-bs-dismiss="modal"
99
+ >
100
+ Close
101
+ </button>
102
+ <button
103
+ type="button"
104
+ class="btn btn-outline-light"
105
+ data-bs-dismiss="modal"
106
+ onclick="updateDeviceName()"
107
+ >
108
+ Save changes
109
+ </button>
77
110
  </div>
78
111
  </div>
79
112
  </div>
@@ -1,47 +1,163 @@
1
- {% extends "nav.html" %}
2
- {% block content %}
1
+ {% extends "nav.html" %} {% block content %}
3
2
  <div class="container-fluid">
4
3
  <div class="row p-2 pt-4 g-4 d-flex justify-content-center">
5
4
  {% if "firmware.write" in request.user.permissions %}
6
- <div class="col col-12 col-lg-6">
7
- <div class="card border rounded">
8
- <div class="card-header">
9
- <h3>Upload SWUpdate File</h3>
10
- </div>
11
- <div class="card-body">
12
- <form id="upload-form">
13
- <div class="row g-3 row-cols-1">
14
- <div class="col">
15
- <input class="form-control" type="file" accept=".swu" id="file-upload" name="file" />
16
- </div>
17
- <div class="col">
18
- <input class="btn btn-primary w-100" id="file-upload-submit" type="submit" value="Upload" />
19
- </div>
20
- <h3>Upload Progress</h3>
21
- <div class="col">
22
- <div class="progress w-100" style=" height: 40px;" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
23
- <div class="progress-bar progress-bar-striped progress-bar-animated" id="upload-progress" style="width: 0%;">0%</div>
24
- </div>
25
- </div>
26
- </div>
27
- </form>
28
- </div>
29
- </div>
5
+ <div class="col col-12">
6
+ <button
7
+ type="button"
8
+ class="btn btn-lg btn-outline-light w-100"
9
+ data-bs-toggle="modal"
10
+ data-bs-target="#upload-modal"
11
+ >
12
+ Upload
13
+ </button>
30
14
  </div>
31
15
  {% endif %}
32
- <div class="col-12 col-lg-6">
33
- <div class="card border rounded">
34
- <div class="card-header">
35
- <h3>Firmware</h3>
36
- </div>
37
- <div class="card-body p-3">
38
- <ul class="list-group" id="firmware-list">
16
+ <div class="col">
17
+ <table id="firmware-table" class="table table-hover">
18
+ <thead>
19
+ <tr>
20
+ <th>ID</th>
21
+ <th>Name</th>
22
+ <th>Version</th>
23
+ <th>Size</th>
24
+ </tr>
25
+ </thead>
26
+ <tbody id="firmware-list"></tbody>
27
+ </table>
28
+ </div>
29
+ </div>
30
+ </div>
39
31
 
40
- </ul>
32
+ {% if "firmware.write" in request.user.permissions %}
33
+ <div class="modal modal-lg fade" id="upload-modal">
34
+ <div class="modal-dialog modal-dialog-centered modal-xl">
35
+ <div class="modal-content">
36
+ <div class="modal-header">
37
+ <ul
38
+ class="nav nav-underline nav-justified w-100"
39
+ role="tablist"
40
+ >
41
+ <li class="nav-item">
42
+ <button
43
+ class="nav-link active"
44
+ aria-current="page"
45
+ id="upload-tab"
46
+ data-bs-toggle="tab"
47
+ data-bs-target="#upload-tab-content"
48
+ type="button"
49
+ role="tab"
50
+ >
51
+ Upload File
52
+ </button>
53
+ </li>
54
+ <li class="nav-item">
55
+ <button
56
+ class="nav-link"
57
+ id="url-tab"
58
+ data-bs-toggle="tab"
59
+ data-bs-target="#url-tab-content"
60
+ type="button"
61
+ role="tab"
62
+ >
63
+ Remote URL
64
+ </button>
65
+ </li>
66
+ </ul>
67
+ </div>
68
+ <div class="tab-content">
69
+ <div class="tab-pane active" id="upload-tab-content">
70
+ <div class="modal-body">
71
+ <form id="upload-form">
72
+ <div class="row g-3 row-cols-1">
73
+ <div id="upload-alerts"></div>
74
+ <div class="col">
75
+ <input
76
+ class="form-control"
77
+ type="file"
78
+ accept=".swu"
79
+ id="file-upload"
80
+ name="file"
81
+ />
82
+ </div>
83
+ <div class="col">
84
+ <input
85
+ class="btn btn-outline-light w-100"
86
+ id="file-upload-submit"
87
+ type="submit"
88
+ value="Upload"
89
+ />
90
+ <div
91
+ class="progress border border-light bg-dark d-none"
92
+ style="height: 36px"
93
+ role="progressbar"
94
+ aria-valuenow="0"
95
+ aria-valuemin="0"
96
+ aria-valuemax="100"
97
+ >
98
+ <div
99
+ class="progress-bar progress-bar-striped bg-success progress-bar-animated"
100
+ id="upload-progress"
101
+ style="width: 0%"
102
+ >
103
+ 0%
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </form>
109
+ </div>
110
+ </div>
111
+ <div class="tab-pane" id="url-tab-content">
112
+ <div class="modal-body">
113
+ <form id="url-form">
114
+ <div class="row g-3 row-cols-1">
115
+ <div id="url-alerts"></div>
116
+ <div class="col">
117
+ <div class="input-group">
118
+ <span class="input-group-text"
119
+ >File URL</span
120
+ >
121
+ <input
122
+ class="form-control"
123
+ type="url"
124
+ id="file-url"
125
+ />
126
+ </div>
127
+ </div>
128
+ <div class="col">
129
+ <input
130
+ class="btn btn-outline-light w-100"
131
+ id="url-submit"
132
+ type="submit"
133
+ value="Upload"
134
+ />
135
+ <div
136
+ class="progress border border-light bg-dark d-none"
137
+ style="height: 36px"
138
+ role="progressbar"
139
+ aria-valuenow="0"
140
+ aria-valuemin="0"
141
+ aria-valuemax="100"
142
+ >
143
+ <div
144
+ class="progress-bar progress-bar-striped bg-success progress-bar-animated"
145
+ id="url-progress"
146
+ style="width: 0%"
147
+ >
148
+ 0%
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </form>
154
+ </div>
41
155
  </div>
42
156
  </div>
43
157
  </div>
44
158
  </div>
45
159
  </div>
160
+ {% endif %}
161
+ <script src="{{ url_for('static', path='js/util.js') }}"></script>
46
162
  <script src="{{ url_for('static', path='js/firmware.js') }}"></script>
47
163
  {% endblock content %}
@@ -1,34 +1,20 @@
1
- {% extends "nav.html" %}
2
- {% block content %}
1
+ {% extends "nav.html" %} {% block content %}
3
2
  <div class="container-fluid">
4
3
  <div class="row p-2 d-flex justify-content-center">
5
4
  <div class="col">
6
5
  <table id="device-table" class="table table-hover">
7
6
  <thead>
8
7
  <tr>
9
- <th>
10
- Name
11
- </th>
12
- <th>
13
- Up
14
- </th>
15
- <th>
16
- UUID
17
- </th>
18
- <th>
19
- Firmware
20
- </th>
21
- <th>
22
- Last IP
23
- </th>
24
- <th>
25
- Last Seen
26
- </th>
8
+ <th>Name</th>
9
+ <th>Up</th>
10
+ <th>UUID</th>
11
+ <th>Firmware</th>
12
+ <th>Progress</th>
13
+ <th>Last IP</th>
14
+ <th>Last Seen</th>
27
15
  </tr>
28
16
  </thead>
29
- <tbody id="devices-list">
30
-
31
- </tbody>
17
+ <tbody id="devices-list"></tbody>
32
18
  </table>
33
19
  </div>
34
20
  </div>