goosebit 0.2.3__py3-none-any.whl → 0.2.5__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 (50) hide show
  1. goosebit/__init__.py +32 -3
  2. goosebit/api/v1/devices/device/routes.py +10 -4
  3. goosebit/api/v1/devices/responses.py +0 -7
  4. goosebit/api/v1/devices/routes.py +19 -3
  5. goosebit/api/v1/rollouts/responses.py +2 -7
  6. goosebit/api/v1/rollouts/routes.py +7 -3
  7. goosebit/api/v1/software/responses.py +0 -7
  8. goosebit/api/v1/software/routes.py +24 -11
  9. goosebit/auth/__init__.py +12 -8
  10. goosebit/db/__init__.py +12 -1
  11. goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
  12. goosebit/db/models.py +19 -4
  13. goosebit/realtime/logs.py +1 -1
  14. goosebit/schema/devices.py +42 -38
  15. goosebit/schema/rollouts.py +21 -18
  16. goosebit/schema/software.py +24 -19
  17. goosebit/settings/schema.py +2 -0
  18. goosebit/ui/bff/common/__init__.py +0 -0
  19. goosebit/ui/bff/common/requests.py +44 -0
  20. goosebit/ui/bff/common/responses.py +16 -0
  21. goosebit/ui/bff/common/util.py +32 -0
  22. goosebit/ui/bff/devices/responses.py +15 -19
  23. goosebit/ui/bff/devices/routes.py +61 -7
  24. goosebit/ui/bff/rollouts/responses.py +15 -19
  25. goosebit/ui/bff/rollouts/routes.py +8 -6
  26. goosebit/ui/bff/routes.py +4 -2
  27. goosebit/ui/bff/software/responses.py +29 -19
  28. goosebit/ui/bff/software/routes.py +29 -16
  29. goosebit/ui/nav.py +1 -1
  30. goosebit/ui/routes.py +10 -19
  31. goosebit/ui/static/js/devices.js +188 -94
  32. goosebit/ui/static/js/rollouts.js +20 -13
  33. goosebit/ui/static/js/software.js +5 -11
  34. goosebit/ui/static/js/util.js +43 -14
  35. goosebit/ui/templates/devices.html.jinja +77 -49
  36. goosebit/ui/templates/nav.html.jinja +35 -4
  37. goosebit/ui/templates/rollouts.html.jinja +23 -23
  38. goosebit/updater/controller/v1/routes.py +33 -23
  39. goosebit/updater/controller/v1/schema.py +4 -4
  40. goosebit/updater/manager.py +28 -52
  41. goosebit/updater/routes.py +6 -2
  42. goosebit/updates/__init__.py +14 -21
  43. goosebit/updates/swdesc.py +36 -15
  44. {goosebit-0.2.3.dist-info → goosebit-0.2.5.dist-info}/METADATA +23 -7
  45. {goosebit-0.2.3.dist-info → goosebit-0.2.5.dist-info}/RECORD +48 -44
  46. {goosebit-0.2.3.dist-info → goosebit-0.2.5.dist-info}/WHEEL +1 -1
  47. goosebit-0.2.5.dist-info/entry_points.txt +3 -0
  48. goosebit/ui/static/js/index.js +0 -155
  49. goosebit/ui/templates/index.html.jinja +0 -25
  50. {goosebit-0.2.3.dist-info → goosebit-0.2.5.dist-info}/LICENSE +0 -0
@@ -1,29 +1,70 @@
1
1
  let dataTable;
2
2
 
3
+ const renderFunctions = {
4
+ online: (data, type) => {
5
+ if (type === "display" || type === "filter") {
6
+ const color = data ? "success" : "danger";
7
+ return `
8
+ <div class="text-${color}">
9
+
10
+ </div>
11
+ `;
12
+ }
13
+ return data;
14
+ },
15
+ force_update: (data, type) => {
16
+ if (type === "display" || type === "filter") {
17
+ const color = data ? "success" : "muted";
18
+ return `
19
+ <div class="text-${color}">
20
+
21
+ </div>
22
+ `;
23
+ }
24
+ return data;
25
+ },
26
+ progress: (data, type) => {
27
+ if (type === "display" || type === "filter") {
28
+ return data ? `${data}%` : "-";
29
+ }
30
+ return data;
31
+ },
32
+ last_seen: (data, type) => {
33
+ if (type === "display" || type === "filter") {
34
+ return secondsToRecentDate(data);
35
+ }
36
+ return data;
37
+ },
38
+ };
39
+
3
40
  document.addEventListener("DOMContentLoaded", async () => {
41
+ const columnConfig = await get_request("/ui/bff/devices/columns");
42
+ for (const col in columnConfig.columns) {
43
+ const colDesc = columnConfig.columns[col];
44
+ const colName = colDesc.data;
45
+ if (renderFunctions[colName]) {
46
+ columnConfig.columns[col].render = renderFunctions[colName];
47
+ }
48
+ }
49
+
4
50
  dataTable = new DataTable("#device-table", {
5
51
  responsive: true,
6
52
  paging: true,
7
53
  processing: false,
8
54
  serverSide: true,
9
- order: [],
55
+ order: { name: "uuid", dir: "asc" },
10
56
  scrollCollapse: true,
11
57
  scroller: true,
12
58
  scrollY: "65vh",
13
59
  stateSave: true,
14
- stateLoadParams: (settings, data) => {
15
- // if save state is older than last breaking code change...
16
- if (data.time <= 1722434189000) {
17
- // ... delete it
18
- for (const key of Object.keys(data)) {
19
- delete data[key];
20
- }
21
- }
22
- },
23
60
  select: true,
24
61
  rowId: "uuid",
25
62
  ajax: {
26
- url: "/ui/bff/devices/",
63
+ url: "/ui/bff/devices",
64
+ data: (data) => {
65
+ // biome-ignore lint/performance/noDelete: really has to be deleted
66
+ delete data.columns;
67
+ },
27
68
  contentType: "application/json",
28
69
  },
29
70
  initComplete: () => {
@@ -37,64 +78,7 @@ document.addEventListener("DOMContentLoaded", async () => {
37
78
  render: (data) => data || "-",
38
79
  },
39
80
  ],
40
- columns: [
41
- {
42
- data: "online",
43
- render: (data, type) => {
44
- if (type === "display" || type === "filter") {
45
- const color = data ? "success" : "danger";
46
- return `
47
- <div class="text-${color}">
48
-
49
- </div>
50
- `;
51
- }
52
- return data;
53
- },
54
- },
55
- { data: "uuid", searchable: true, orderable: true },
56
- { data: "name", searchable: true, orderable: true },
57
- { data: "hw_model" },
58
- { data: "hw_revision" },
59
- { data: "feed", searchable: true, orderable: true },
60
- { data: "sw_version", searchable: true, orderable: true },
61
- { data: "sw_target_version" },
62
- { data: "update_mode", searchable: true, orderable: true },
63
- { data: "last_state", searchable: true, orderable: true },
64
- {
65
- data: "force_update",
66
- render: (data, type) => {
67
- if (type === "display" || type === "filter") {
68
- const color = data ? "success" : "muted";
69
- return `
70
- <div class="text-${color}">
71
-
72
- </div>
73
- `;
74
- }
75
- return data;
76
- },
77
- },
78
- {
79
- data: "progress",
80
- render: (data, type) => {
81
- if (type === "display" || type === "filter") {
82
- return data ? `${data}%` : "-";
83
- }
84
- return data;
85
- },
86
- },
87
- { data: "last_ip" },
88
- {
89
- data: "last_seen",
90
- render: (data, type) => {
91
- if (type === "display" || type === "filter") {
92
- return secondsToRecentDate(data);
93
- }
94
- return data;
95
- },
96
- },
97
- ],
81
+ columns: columnConfig.columns,
98
82
  layout: {
99
83
  top1Start: {
100
84
  buttons: [
@@ -124,20 +108,11 @@ document.addEventListener("DOMContentLoaded", async () => {
124
108
  {
125
109
  text: '<i class="bi bi-pen" ></i>',
126
110
  action: () => {
127
- const selectedDevice = dataTable.rows({ selected: true }).data().toArray()[0];
128
- $("#device-selected-name").val(selectedDevice.name);
111
+ const selectedDevices = dataTable.rows({ selected: true }).data().toArray();
112
+ const selectedDevice = selectedDevices[0];
113
+ updateSoftwareSelection(selectedDevices);
114
+ $("#device-name").val(selectedDevice.name);
129
115
  $("#device-selected-feed").val(selectedDevice.feed);
130
-
131
- let selectedValue;
132
- if (selectedDevice.update_mode === "Rollout") {
133
- selectedValue = "rollout";
134
- } else if (selectedDevice.update_mode === "Latest") {
135
- selectedValue = "latest";
136
- } else {
137
- selectedValue = selectedDevice.sw_assigned;
138
- }
139
- $("#selected-sw").val(selectedValue);
140
-
141
116
  new bootstrap.Modal("#device-config-modal").show();
142
117
  },
143
118
  className: "buttons-config",
@@ -199,22 +174,89 @@ document.addEventListener("DOMContentLoaded", async () => {
199
174
  dataTable.ajax.reload(null, false);
200
175
  }, TABLE_UPDATE_TIME);
201
176
 
202
- await updateSoftwareSelection(true);
177
+ await updateSoftwareSelection();
203
178
 
204
- // Config form submit
205
- const configForm = document.getElementById("device-config-form");
206
- configForm.addEventListener(
179
+ // Name update form submit
180
+ const nameForm = document.getElementById("device-name-form");
181
+ nameForm.addEventListener(
207
182
  "submit",
208
183
  async (event) => {
209
- if (configForm.checkValidity() === false) {
184
+ if (nameForm.checkValidity() === false) {
210
185
  event.preventDefault();
211
186
  event.stopPropagation();
212
- configForm.classList.add("was-validated");
187
+ nameForm.classList.add("was-validated");
213
188
  } else {
214
189
  event.preventDefault();
215
- await updateDeviceConfig();
216
- configForm.classList.remove("was-validated");
217
- configForm.reset();
190
+ await updateDeviceName();
191
+ nameForm.classList.remove("was-validated");
192
+ nameForm.reset();
193
+ const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
194
+ modal.hide();
195
+ }
196
+ },
197
+ false,
198
+ );
199
+
200
+ // Rollout form submit
201
+ const rolloutForm = document.getElementById("device-software-rollout-form");
202
+ rolloutForm.addEventListener(
203
+ "submit",
204
+ async (event) => {
205
+ if (rolloutForm.checkValidity() === false) {
206
+ event.preventDefault();
207
+ event.stopPropagation();
208
+ rolloutForm.classList.add("was-validated");
209
+ } else {
210
+ event.preventDefault();
211
+ await updateDeviceRollout();
212
+ rolloutForm.classList.remove("was-validated");
213
+ rolloutForm.reset();
214
+ const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
215
+ modal.hide();
216
+ }
217
+ },
218
+ false,
219
+ );
220
+
221
+ // Manual software form submit
222
+ const manualSoftwareForm = document.getElementById("device-software-manual-form");
223
+ manualSoftwareForm.addEventListener(
224
+ "submit",
225
+ async (event) => {
226
+ if (manualSoftwareForm.checkValidity() === false) {
227
+ event.preventDefault();
228
+ event.stopPropagation();
229
+ manualSoftwareForm.classList.add("was-validated");
230
+ if (document.getElementById("selected-sw").value === "") {
231
+ document.getElementById("selected-sw").parentElement.classList.add("is-invalid");
232
+ }
233
+ } else {
234
+ event.preventDefault();
235
+ await updateDeviceManualSoftware();
236
+ manualSoftwareForm.classList.remove("was-validated");
237
+ document.getElementById("selected-sw").parentElement.classList.remove("is-invalid");
238
+ manualSoftwareForm.reset();
239
+ const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
240
+ modal.hide();
241
+ }
242
+ },
243
+ false,
244
+ );
245
+
246
+ // Latest software form submit
247
+ const latestSoftwareForm = document.getElementById("device-software-latest-form");
248
+ latestSoftwareForm.addEventListener(
249
+ "submit",
250
+ async (event) => {
251
+ if (latestSoftwareForm.checkValidity() === false) {
252
+ event.preventDefault();
253
+ event.stopPropagation();
254
+ latestSoftwareForm.classList.add("was-validated");
255
+ } else {
256
+ event.preventDefault();
257
+ await updateDeviceLatest();
258
+ latestSoftwareForm.classList.remove("was-validated");
259
+ latestSoftwareForm.reset();
218
260
  const modal = bootstrap.Modal.getInstance(document.getElementById("device-config-modal"));
219
261
  modal.hide();
220
262
  }
@@ -244,18 +286,70 @@ function updateBtnState() {
244
286
  }
245
287
  }
246
288
 
247
- async function updateDeviceConfig() {
289
+ async function updateDeviceName() {
290
+ const devices = dataTable
291
+ .rows({ selected: true })
292
+ .data()
293
+ .toArray()
294
+ .map((d) => d.uuid);
295
+ const name = document.getElementById("device-name").value;
296
+
297
+ try {
298
+ await patch_request("/ui/bff/devices", { devices, name });
299
+ } catch (error) {
300
+ console.error("Update device config failed:", error);
301
+ }
302
+
303
+ setTimeout(updateDeviceList, 50);
304
+ }
305
+
306
+ async function updateDeviceRollout() {
248
307
  const devices = dataTable
249
308
  .rows({ selected: true })
250
309
  .data()
251
310
  .toArray()
252
311
  .map((d) => d.uuid);
253
- const name = document.getElementById("device-selected-name").value;
254
312
  const feed = document.getElementById("device-selected-feed").value;
313
+ const software = "rollout";
314
+
315
+ try {
316
+ await patch_request("/ui/bff/devices", { devices, feed, software });
317
+ } catch (error) {
318
+ console.error("Update device config failed:", error);
319
+ }
320
+
321
+ setTimeout(updateDeviceList, 50);
322
+ }
323
+
324
+ async function updateDeviceManualSoftware() {
325
+ const devices = dataTable
326
+ .rows({ selected: true })
327
+ .data()
328
+ .toArray()
329
+ .map((d) => d.uuid);
330
+ const feed = null;
255
331
  const software = document.getElementById("selected-sw").value;
256
332
 
257
333
  try {
258
- await patch_request("/ui/bff/devices", { devices, name, feed, software });
334
+ await patch_request("/ui/bff/devices", { devices, feed, software });
335
+ } catch (error) {
336
+ console.error("Update device config failed:", error);
337
+ }
338
+
339
+ setTimeout(updateDeviceList, 50);
340
+ }
341
+
342
+ async function updateDeviceLatest() {
343
+ const devices = dataTable
344
+ .rows({ selected: true })
345
+ .data()
346
+ .toArray()
347
+ .map((d) => d.uuid);
348
+ const feed = null;
349
+ const software = "latest";
350
+
351
+ try {
352
+ await patch_request("/ui/bff/devices", { devices, feed, software });
259
353
  } catch (error) {
260
354
  console.error("Update device config failed:", error);
261
355
  }
@@ -6,24 +6,22 @@ document.addEventListener("DOMContentLoaded", async () => {
6
6
  paging: true,
7
7
  processing: true,
8
8
  serverSide: true,
9
- order: [1, "desc"],
9
+ order: {
10
+ name: "created_at",
11
+ dir: "desc",
12
+ },
10
13
  scrollCollapse: true,
11
14
  scroller: true,
12
15
  scrollY: "65vh",
13
16
  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
17
  select: true,
24
18
  rowId: "id",
25
19
  ajax: {
26
20
  url: "/ui/bff/rollouts",
21
+ data: (data) => {
22
+ // biome-ignore lint/performance/noDelete: really has to be deleted
23
+ delete data.columns;
24
+ },
27
25
  contentType: "application/json",
28
26
  },
29
27
  initComplete: () => {
@@ -38,9 +36,14 @@ document.addEventListener("DOMContentLoaded", async () => {
38
36
  ],
39
37
  columns: [
40
38
  { data: "id", visible: false },
41
- { data: "created_at", orderable: true, render: (data) => new Date(data).toLocaleString() },
42
- { data: "name", searchable: true, orderable: true },
43
- { data: "feed", searchable: true, orderable: true },
39
+ {
40
+ data: "created_at",
41
+ name: "created_at",
42
+ orderable: true,
43
+ render: (data) => new Date(data).toLocaleString(),
44
+ },
45
+ { data: "name", name: "name", searchable: true, orderable: true },
46
+ { data: "feed", name: "feed", searchable: true, orderable: true },
44
47
  { data: "sw_file" },
45
48
  { data: "sw_version" },
46
49
  {
@@ -136,6 +139,9 @@ document.addEventListener("DOMContentLoaded", async () => {
136
139
  "submit",
137
140
  (event) => {
138
141
  if (form.checkValidity() === false) {
142
+ if (document.getElementById("selected-sw").value === "") {
143
+ document.getElementById("selected-sw").parentElement.classList.add("is-invalid");
144
+ }
139
145
  event.preventDefault();
140
146
  event.stopPropagation();
141
147
  form.classList.add("was-validated");
@@ -143,6 +149,7 @@ document.addEventListener("DOMContentLoaded", async () => {
143
149
  event.preventDefault();
144
150
  createRollout();
145
151
  form.classList.remove("was-validated");
152
+ document.getElementById("selected-sw").parentElement.classList.remove("is-invalid");
146
153
  form.reset();
147
154
  const modal = bootstrap.Modal.getInstance(document.getElementById("rollout-create-modal"));
148
155
  modal.hide();
@@ -172,22 +172,16 @@ document.addEventListener("DOMContentLoaded", () => {
172
172
  paging: true,
173
173
  processing: false,
174
174
  serverSide: true,
175
- order: [2, "desc"],
176
175
  scrollCollapse: true,
177
176
  scroller: true,
178
177
  scrollY: "60vh",
179
178
  stateSave: true,
180
- stateLoadParams: (settings, data) => {
181
- // if save state is older than last breaking code change...
182
- if (data.time <= 1722415428000) {
183
- // ... delete it
184
- for (const key of Object.keys(data)) {
185
- delete data[key];
186
- }
187
- }
188
- },
189
179
  ajax: {
190
180
  url: "/ui/bff/software",
181
+ data: (data) => {
182
+ // biome-ignore lint/performance/noDelete: really has to be deleted
183
+ delete data.columns;
184
+ },
191
185
  contentType: "application/json",
192
186
  },
193
187
  initComplete: () => {
@@ -204,7 +198,7 @@ document.addEventListener("DOMContentLoaded", () => {
204
198
  columns: [
205
199
  { data: "id", visible: false },
206
200
  { data: "name" },
207
- { data: "version", searchable: true, orderable: true },
201
+ { data: "version", name: "version", searchable: true, orderable: true },
208
202
  {
209
203
  data: "compatibility",
210
204
  render: (data) => {
@@ -20,27 +20,22 @@ function secondsToRecentDate(t) {
20
20
  return s + (s === 1 ? " second" : " seconds");
21
21
  }
22
22
 
23
- async function updateSoftwareSelection(addSpecialMode = false) {
23
+ async function updateSoftwareSelection(devices = null) {
24
24
  try {
25
- const response = await fetch("/ui/bff/software");
25
+ const url = new URL("/ui/bff/software?order[0][dir]=desc&order[0][name]=version", window.location.origin);
26
+ if (devices != null) {
27
+ for (const device of devices) {
28
+ url.searchParams.append("uuids", device.uuid);
29
+ }
30
+ }
31
+ const response = await fetch(url.toString());
26
32
  if (!response.ok) {
27
33
  console.error("Retrieving software list failed.");
28
34
  return;
29
35
  }
30
36
  const data = (await response.json()).data;
31
37
  const selectElem = document.getElementById("selected-sw");
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
- }
38
+ selectElem.innerHTML = "";
44
39
 
45
40
  for (const item of data) {
46
41
  const optionElem = document.createElement("option");
@@ -50,11 +45,45 @@ async function updateSoftwareSelection(addSpecialMode = false) {
50
45
  optionElem.textContent = `${item.version} (${models})`;
51
46
  selectElem.appendChild(optionElem);
52
47
  }
48
+ $("#selected-sw").selectpicker("destroy");
49
+ if (data.length === 0) {
50
+ selectElem.title = "No valid software found for selected device";
51
+ if (devices != null) {
52
+ if (devices.length > 1) {
53
+ selectElem.title += "s";
54
+ }
55
+ }
56
+ selectElem.disabled = true;
57
+ } else {
58
+ selectElem.disabled = false;
59
+ selectElem.title = "Select Software";
60
+ }
61
+ $("#selected-sw").selectpicker();
53
62
  } catch (error) {
54
63
  console.error("Failed to fetch device data:", error);
55
64
  }
56
65
  }
57
66
 
67
+ async function get_request(url) {
68
+ const response = await fetch(url, {
69
+ method: "GET",
70
+ });
71
+
72
+ const result = await response.json();
73
+ if (!response.ok) {
74
+ if (result.detail) {
75
+ Swal.fire({
76
+ title: "Warning",
77
+ text: result.detail,
78
+ icon: "warning",
79
+ confirmButtonText: "Understood",
80
+ });
81
+ }
82
+
83
+ throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
84
+ }
85
+ return result;
86
+ }
58
87
  async function post_request(url, object) {
59
88
  const response = await fetch(url, {
60
89
  method: "POST",