zebra-day 1.0.2__py3-none-any.whl → 2.0.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 (86) hide show
  1. zebra_day/cli/gui.py +89 -6
  2. zebra_day/etc/printer_config.json +2 -14
  3. zebra_day/etc/printer_config.template.json +17 -9
  4. zebra_day/etc/tmp_printers120.json +10 -0
  5. zebra_day/etc/tmp_printers145.json +10 -0
  6. zebra_day/etc/tmp_printers207.json +10 -0
  7. zebra_day/etc/tmp_printers469.json +10 -0
  8. zebra_day/etc/tmp_printers485.json +10 -0
  9. zebra_day/etc/tmp_printers531.json +10 -0
  10. zebra_day/etc/tmp_printers540.json +10 -0
  11. zebra_day/etc/tmp_printers542.json +10 -0
  12. zebra_day/etc/tmp_printers552.json +10 -0
  13. zebra_day/etc/tmp_printers715.json +10 -0
  14. zebra_day/etc/tmp_printers972.json +10 -0
  15. zebra_day/files/blank_preview.png +0 -0
  16. zebra_day/files/corners_20cmX30cm_preview.png +0 -0
  17. zebra_day/files/generic_2inX1in_preview.png +0 -0
  18. zebra_day/files/test_png_12020.png +0 -0
  19. zebra_day/files/test_png_12352.png +0 -0
  20. zebra_day/files/test_png_15472.png +0 -0
  21. zebra_day/files/test_png_24493.png +0 -0
  22. zebra_day/files/test_png_30069.png +0 -0
  23. zebra_day/files/test_png_47791.png +0 -0
  24. zebra_day/files/test_png_47799.png +0 -0
  25. zebra_day/files/test_png_55588.png +0 -0
  26. zebra_day/files/test_png_58809.png +0 -0
  27. zebra_day/files/test_png_67242.png +0 -0
  28. zebra_day/files/test_png_89893.png +0 -0
  29. zebra_day/files/tube_20mmX30mmA_preview.png +0 -0
  30. zebra_day/print_mgr.py +136 -80
  31. zebra_day/templates/modern/config_backups.html +59 -0
  32. zebra_day/templates/modern/config_editor.html +95 -0
  33. zebra_day/templates/modern/config_new.html +93 -0
  34. zebra_day/templates/modern/print_request.html +9 -5
  35. zebra_day/templates/modern/printer_detail.html +161 -34
  36. zebra_day/templates/modern/printers.html +17 -6
  37. zebra_day/templates/modern/template_editor.html +7 -4
  38. zebra_day/web/app.py +84 -7
  39. zebra_day/web/routers/api.py +155 -5
  40. zebra_day/web/routers/ui.py +155 -570
  41. {zebra_day-1.0.2.dist-info → zebra_day-2.0.0.dist-info}/METADATA +74 -13
  42. {zebra_day-1.0.2.dist-info → zebra_day-2.0.0.dist-info}/RECORD +46 -57
  43. zebra_day/bin/fetch_zebra_config.py +0 -15
  44. zebra_day/bin/generate_coord_grid_zpl.py +0 -50
  45. zebra_day/bin/print_zpl_from_file.py +0 -21
  46. zebra_day/bin/probe_new_label_dimensions.py +0 -75
  47. zebra_day/bin/scan_for_networed_zebra_printers.py +0 -23
  48. zebra_day/bin/scan_for_networed_zebra_printers_arp_scan.sh +0 -1
  49. zebra_day/bin/scan_for_networed_zebra_printers_curl.sh +0 -30
  50. zebra_day/bin/zserve.py +0 -1062
  51. zebra_day/templates/base.html +0 -36
  52. zebra_day/templates/bpr.html +0 -72
  53. zebra_day/templates/build_new_config.html +0 -36
  54. zebra_day/templates/build_print_request.html +0 -32
  55. zebra_day/templates/chg_ui_style.html +0 -19
  56. zebra_day/templates/edit_template.html +0 -128
  57. zebra_day/templates/edit_zpl.html +0 -37
  58. zebra_day/templates/index.html +0 -82
  59. zebra_day/templates/legacy/base.html +0 -37
  60. zebra_day/templates/legacy/bpr.html +0 -72
  61. zebra_day/templates/legacy/build_new_config.html +0 -36
  62. zebra_day/templates/legacy/build_print_request.html +0 -32
  63. zebra_day/templates/legacy/chg_ui_style.html +0 -19
  64. zebra_day/templates/legacy/edit_template.html +0 -128
  65. zebra_day/templates/legacy/edit_zpl.html +0 -37
  66. zebra_day/templates/legacy/index.html +0 -82
  67. zebra_day/templates/legacy/list_prior_configs.html +0 -24
  68. zebra_day/templates/legacy/print_result.html +0 -30
  69. zebra_day/templates/legacy/printer_details.html +0 -25
  70. zebra_day/templates/legacy/printer_status.html +0 -70
  71. zebra_day/templates/legacy/save_result.html +0 -17
  72. zebra_day/templates/legacy/send_print_request.html +0 -34
  73. zebra_day/templates/legacy/simple_print.html +0 -94
  74. zebra_day/templates/legacy/view_pstation_json.html +0 -29
  75. zebra_day/templates/list_prior_configs.html +0 -24
  76. zebra_day/templates/print_result.html +0 -30
  77. zebra_day/templates/printer_details.html +0 -25
  78. zebra_day/templates/printer_status.html +0 -70
  79. zebra_day/templates/save_result.html +0 -17
  80. zebra_day/templates/send_print_request.html +0 -34
  81. zebra_day/templates/simple_print.html +0 -94
  82. zebra_day/templates/view_pstation_json.html +0 -29
  83. {zebra_day-1.0.2.dist-info → zebra_day-2.0.0.dist-info}/WHEEL +0 -0
  84. {zebra_day-1.0.2.dist-info → zebra_day-2.0.0.dist-info}/entry_points.txt +0 -0
  85. {zebra_day-1.0.2.dist-info → zebra_day-2.0.0.dist-info}/licenses/LICENSE +0 -0
  86. {zebra_day-1.0.2.dist-info → zebra_day-2.0.0.dist-info}/top_level.txt +0 -0
@@ -5,7 +5,7 @@
5
5
  {% block content %}
6
6
  <div class="page-header">
7
7
  <h1 class="page-title">{{ printer_name }}</h1>
8
- <p class="page-subtitle">Printer details for {{ lab }}</p>
8
+ <p class="page-subtitle">Printer details for {{ lab_name | default(lab) }}</p>
9
9
  </div>
10
10
 
11
11
  <!-- Breadcrumb -->
@@ -14,7 +14,7 @@
14
14
  <span class="text-muted"> / </span>
15
15
  <a href="/printers">Printers</a>
16
16
  <span class="text-muted"> / </span>
17
- <a href="/printers/{{ lab }}">{{ lab }}</a>
17
+ <a href="/printers/{{ lab }}">{{ lab_name | default(lab) }}</a>
18
18
  <span class="text-muted"> / </span>
19
19
  <span>{{ printer_name }}</span>
20
20
  </div>
@@ -32,53 +32,149 @@
32
32
  </div>
33
33
  <table class="table" style="background: transparent; box-shadow: none;">
34
34
  <tbody>
35
- {% for key, value in printer_info.items() %}
36
35
  <tr>
37
- <td style="width: 40%;"><strong>{{ key }}</strong></td>
36
+ <td style="width: 40%;"><strong>Printer ID</strong></td>
37
+ <td>{{ printer_id }}</td>
38
+ </tr>
39
+ <tr>
40
+ <td><strong>Display Name</strong></td>
41
+ <td>{{ printer_info.printer_name | default('Not set', true) }}</td>
42
+ </tr>
43
+ <tr>
44
+ <td><strong>Location</strong></td>
45
+ <td>{{ printer_info.lab_location | default('Not set', true) }}</td>
46
+ </tr>
47
+ <tr>
48
+ <td><strong>Manufacturer</strong></td>
49
+ <td>{{ printer_info.manufacturer | default('zebra') | title }}</td>
50
+ </tr>
51
+ <tr>
52
+ <td><strong>Model</strong></td>
53
+ <td>{{ printer_info.model | default('Unknown') }}</td>
54
+ </tr>
55
+ <tr>
56
+ <td><strong>Serial</strong></td>
57
+ <td>{{ printer_info.serial | default('Unknown') }}</td>
58
+ </tr>
59
+ <tr>
60
+ <td><strong>IP Address</strong></td>
38
61
  <td>
39
- {% if key == 'ip_address' and value != 'dl_png' %}
40
- <a href="http://{{ value }}" target="_blank">{{ value }}</a>
41
- {% elif key == 'status' %}
42
- {% if value == 'online' %}
43
- <span class="badge badge-success">Online</span>
62
+ {% if printer_info.ip_address != 'dl_png' %}
63
+ <a href="http://{{ printer_info.ip_address }}" target="_blank">{{ printer_info.ip_address }}</a>
44
64
  {% else %}
45
- <span class="badge badge-error">Offline</span>
65
+ {{ printer_info.ip_address }}
46
66
  {% endif %}
47
- {% elif key == 'label_zpl_styles' %}
48
- {% for style in value %}
67
+ </td>
68
+ </tr>
69
+ <tr>
70
+ <td><strong>Print Method</strong></td>
71
+ <td>{{ printer_info.print_method | default('socket') }}</td>
72
+ </tr>
73
+ <tr>
74
+ <td><strong>Label Styles</strong></td>
75
+ <td>
76
+ {% for style in printer_info.label_zpl_styles %}
49
77
  <span class="badge badge-primary">{{ style }}</span>
50
78
  {% endfor %}
79
+ </td>
80
+ </tr>
81
+ <tr>
82
+ <td><strong>Default Label Style</strong></td>
83
+ <td>
84
+ {% if printer_info.default_label_style %}
85
+ <span class="badge badge-success">{{ printer_info.default_label_style }}</span>
51
86
  {% else %}
52
- {{ value }}
87
+ <span class="text-muted">{{ printer_info.label_zpl_styles[0] if printer_info.label_zpl_styles else 'None' }} (first in list)</span>
53
88
  {% endif %}
54
89
  </td>
55
90
  </tr>
56
- {% endfor %}
91
+ {% if printer_info.notes %}
92
+ <tr>
93
+ <td><strong>Notes</strong></td>
94
+ <td>{{ printer_info.notes }}</td>
95
+ </tr>
96
+ {% endif %}
57
97
  </tbody>
58
98
  </table>
59
99
  </div>
60
-
61
- <!-- Quick Actions -->
100
+
101
+ <!-- Edit Printer Settings -->
62
102
  <div class="card">
63
103
  <div class="card-header">
64
- <h3 class="card-title">Quick Actions</h3>
65
- </div>
66
- <div class="quick-actions">
67
- <a href="/print?lab={{ lab }}&printer={{ printer_name }}" class="action-card">
68
- <i class="fas fa-print"></i>
69
- <div>
70
- <strong>Print Label</strong>
71
- <small class="text-muted d-block">Send a print request</small>
72
- </div>
73
- </a>
74
- <a href="/templates" class="action-card">
75
- <i class="fas fa-file-code"></i>
76
- <div>
77
- <strong>View Templates</strong>
78
- <small class="text-muted d-block">Available label styles</small>
79
- </div>
80
- </a>
104
+ <h3 class="card-title">Edit Settings</h3>
81
105
  </div>
106
+ <form id="printer-edit-form" class="form-group">
107
+ <div class="form-group">
108
+ <label class="form-label">Display Name</label>
109
+ <input type="text" name="printer_name" class="form-control"
110
+ value="{{ printer_info.printer_name | default('', true) }}"
111
+ placeholder="e.g., Lab 3 - Bench A">
112
+ <small class="text-muted">User-friendly name for this printer</small>
113
+ </div>
114
+ <div class="form-group">
115
+ <label class="form-label">Location</label>
116
+ {% if available_locations %}
117
+ <select name="lab_location" class="form-control">
118
+ <option value="">-- Select Location --</option>
119
+ {% for loc in available_locations %}
120
+ <option value="{{ loc }}" {% if printer_info.lab_location == loc %}selected{% endif %}>{{ loc }}</option>
121
+ {% endfor %}
122
+ </select>
123
+ {% else %}
124
+ <input type="text" name="lab_location" class="form-control"
125
+ value="{{ printer_info.lab_location | default('', true) }}"
126
+ placeholder="No locations defined for this lab">
127
+ <small class="text-muted">Define locations at the lab level first</small>
128
+ {% endif %}
129
+ </div>
130
+ <div class="form-group">
131
+ <label class="form-label">Default Label Style</label>
132
+ <select name="default_label_style" class="form-control">
133
+ <option value="">-- Use first in list --</option>
134
+ {% for style in printer_info.label_zpl_styles %}
135
+ <option value="{{ style }}" {% if printer_info.default_label_style == style %}selected{% endif %}>{{ style }}</option>
136
+ {% endfor %}
137
+ </select>
138
+ <small class="text-muted">Style used when printing without specifying a template</small>
139
+ </div>
140
+ <div class="form-group">
141
+ <label class="form-label">Notes</label>
142
+ <textarea name="notes" class="form-control" rows="2" placeholder="Optional notes">{{ printer_info.notes | default('', true) }}</textarea>
143
+ </div>
144
+ <button type="submit" class="btn btn-primary">
145
+ <i class="fas fa-save"></i> Save Changes
146
+ </button>
147
+ </form>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Quick Actions -->
152
+ <div class="card mt-lg">
153
+ <div class="card-header">
154
+ <h3 class="card-title">Quick Actions</h3>
155
+ </div>
156
+ <div class="grid grid-3">
157
+ <a href="/print?lab={{ lab }}&printer={{ printer_id }}" class="action-card">
158
+ <i class="fas fa-print"></i>
159
+ <div>
160
+ <strong>Print Label</strong>
161
+ <small class="text-muted d-block">Send a print request</small>
162
+ </div>
163
+ </a>
164
+ <a href="/templates" class="action-card">
165
+ <i class="fas fa-file-code"></i>
166
+ <div>
167
+ <strong>View Templates</strong>
168
+ <small class="text-muted d-block">Available label styles</small>
169
+ </div>
170
+ </a>
171
+ <a href="/printers/{{ lab }}" class="action-card">
172
+ <i class="fas fa-arrow-left"></i>
173
+ <div>
174
+ <strong>Back to Lab</strong>
175
+ <small class="text-muted d-block">View all printers</small>
176
+ </div>
177
+ </a>
82
178
  </div>
83
179
  </div>
84
180
 
@@ -103,7 +199,7 @@
103
199
  <p class="text-muted mb-lg">Send a test label to verify the printer is working correctly.</p>
104
200
  <div class="grid grid-3">
105
201
  {% for style in printer_info.label_zpl_styles[:6] %}
106
- <a href="/print?lab={{ lab }}&printer={{ printer_name }}&template={{ style }}&test=1" class="action-card">
202
+ <a href="/print?lab={{ lab }}&printer={{ printer_id }}&template={{ style }}&test=1" class="action-card">
107
203
  <i class="fas fa-tag"></i>
108
204
  <div>
109
205
  <strong>{{ style }}</strong>
@@ -113,5 +209,36 @@
113
209
  {% endfor %}
114
210
  </div>
115
211
  </div>
212
+
213
+ <script>
214
+ document.getElementById('printer-edit-form').addEventListener('submit', async (e) => {
215
+ e.preventDefault();
216
+ const form = e.target;
217
+ const data = {
218
+ printer_name: form.printer_name.value || null,
219
+ lab_location: form.lab_location.value || null,
220
+ default_label_style: form.default_label_style.value || null,
221
+ notes: form.notes.value || ''
222
+ };
223
+
224
+ try {
225
+ const response = await fetch('/api/v1/labs/{{ lab }}/printers/{{ printer_id }}', {
226
+ method: 'PATCH',
227
+ headers: { 'Content-Type': 'application/json' },
228
+ body: JSON.stringify(data)
229
+ });
230
+
231
+ if (response.ok) {
232
+ showToast('success', 'Saved', 'Printer settings updated successfully');
233
+ setTimeout(() => window.location.reload(), 1000);
234
+ } else {
235
+ const err = await response.json();
236
+ showToast('error', 'Error', err.detail || 'Failed to save settings');
237
+ }
238
+ } catch (err) {
239
+ showToast('error', 'Error', 'Network error: ' + err.message);
240
+ }
241
+ });
242
+ </script>
116
243
  {% endblock %}
117
244
 
@@ -32,7 +32,7 @@
32
32
  {% if printers %}
33
33
  <div class="card">
34
34
  <div class="card-header">
35
- <h3 class="card-title">Printers{% if lab %} in {{ lab }}{% endif %}</h3>
35
+ <h3 class="card-title">Printers{% if lab %} in {{ lab_name | default(lab) }}{% endif %}</h3>
36
36
  <a href="/config?action=scan&lab={{ lab | default('') }}" class="btn btn-outline btn-sm">
37
37
  <i class="fas fa-sync"></i> Rescan
38
38
  </a>
@@ -42,6 +42,7 @@
42
42
  <thead>
43
43
  <tr>
44
44
  <th>Printer</th>
45
+ <th>Location</th>
45
46
  <th>IP Address</th>
46
47
  <th>Status</th>
47
48
  <th>Label Styles</th>
@@ -53,7 +54,17 @@
53
54
  <tr>
54
55
  <td>
55
56
  <strong>{{ printer.name }}</strong><br>
56
- <small class="text-muted">{{ printer.model | default('Unknown model') }}</small>
57
+ <small class="text-muted">{{ printer.manufacturer | default('zebra') | title }} {{ printer.model | default('Unknown model') }}</small>
58
+ {% if printer.id != printer.name %}
59
+ <br><small class="text-muted">ID: {{ printer.id }}</small>
60
+ {% endif %}
61
+ </td>
62
+ <td>
63
+ {% if printer.lab_location %}
64
+ <span class="badge badge-info">{{ printer.lab_location }}</span>
65
+ {% else %}
66
+ <span class="text-muted">—</span>
67
+ {% endif %}
57
68
  </td>
58
69
  <td>
59
70
  {% if printer.ip_address != 'dl_png' %}
@@ -64,9 +75,9 @@
64
75
  </td>
65
76
  <td>
66
77
  {% if printer.status == 'online' %}
67
- <span class="badge badge-success">Online</span>
78
+ <span class="badge badge-success"><i class="fas fa-check-circle"></i> Online</span>
68
79
  {% else %}
69
- <span class="badge badge-error">Offline</span>
80
+ <span class="badge badge-error"><i class="fas fa-times-circle"></i> Offline</span>
70
81
  {% endif %}
71
82
  </td>
72
83
  <td>
@@ -78,10 +89,10 @@
78
89
  {% endif %}
79
90
  </td>
80
91
  <td>
81
- <a href="/printers/{{ lab }}/{{ printer.name }}" class="btn btn-sm btn-outline">
92
+ <a href="/printers/{{ lab }}/{{ printer.id }}" class="btn btn-sm btn-outline">
82
93
  <i class="fas fa-info-circle"></i> Details
83
94
  </a>
84
- <a href="/print?lab={{ lab }}&printer={{ printer.name }}" class="btn btn-sm btn-primary">
95
+ <a href="/print?lab={{ lab }}&printer={{ printer.id }}" class="btn btn-sm btn-primary">
85
96
  <i class="fas fa-print"></i> Print
86
97
  </a>
87
98
  </td>
@@ -113,11 +113,14 @@
113
113
  var lab = labSelect.value;
114
114
 
115
115
  printerSelect.innerHTML = '<option value="">Select printer...</option>';
116
- if (lab && labsDict[lab]) {
117
- for (var printer in labsDict[lab]) {
116
+ if (lab && labsDict[lab] && labsDict[lab].printers) {
117
+ // v2 schema: printers are nested under 'printers' key
118
+ var printers = labsDict[lab].printers;
119
+ for (var printerId in printers) {
120
+ var printerInfo = printers[printerId];
118
121
  var option = document.createElement('option');
119
- option.value = printer;
120
- option.text = printer;
122
+ option.value = printerId;
123
+ option.text = printerInfo.printer_name || printerId;
121
124
  printerSelect.appendChild(option);
122
125
  }
123
126
  }
zebra_day/web/app.py CHANGED
@@ -141,11 +141,30 @@ def create_app(
141
141
  return app
142
142
 
143
143
 
144
+ def get_default_cert_paths() -> tuple[Optional[Path], Optional[Path]]:
145
+ """
146
+ Get default certificate paths from XDG config directory.
147
+
148
+ Returns:
149
+ Tuple of (cert_path, key_path) or (None, None) if not found.
150
+ """
151
+ config_dir = xdg.get_config_dir()
152
+ cert_dir = config_dir / "certs"
153
+ cert_file = cert_dir / "server.crt"
154
+ key_file = cert_dir / "server.key"
155
+
156
+ if cert_file.exists() and key_file.exists():
157
+ return cert_file, key_file
158
+ return None, None
159
+
160
+
144
161
  def run_server(
145
162
  host: str = "0.0.0.0",
146
163
  port: int = 8118,
147
164
  reload: bool = False,
148
165
  auth: Literal["none", "cognito"] = "none",
166
+ ssl_certfile: Optional[str] = None,
167
+ ssl_keyfile: Optional[str] = None,
149
168
  ):
150
169
  """
151
170
  Run the FastAPI server using uvicorn.
@@ -155,17 +174,75 @@ def run_server(
155
174
  port: Port to listen on
156
175
  reload: Enable auto-reload for development
157
176
  auth: Authentication mode - "none" (public) or "cognito" (AWS Cognito)
177
+ ssl_certfile: Path to SSL certificate file (PEM format)
178
+ ssl_keyfile: Path to SSL private key file (PEM format)
179
+
180
+ If ssl_certfile and ssl_keyfile are not provided, the server will:
181
+ 1. Check SSL_CERT_PATH and SSL_KEY_PATH environment variables
182
+ 2. Check for certificates in ~/.config/zebra_day/certs/
183
+ 3. Fall back to HTTP with a warning if no certificates are found
158
184
  """
159
185
  import uvicorn
160
186
 
161
187
  # Store auth mode in environment for factory function
162
188
  os.environ["ZEBRA_DAY_AUTH_MODE"] = auth
163
189
 
164
- uvicorn.run(
165
- "zebra_day.web.app:create_app",
166
- host=host,
167
- port=port,
168
- reload=reload,
169
- factory=True,
170
- )
190
+ # Resolve SSL certificate paths
191
+ cert_path = ssl_certfile
192
+ key_path = ssl_keyfile
193
+
194
+ # Check environment variables if not provided
195
+ if not cert_path:
196
+ cert_path = os.environ.get("SSL_CERT_PATH")
197
+ if not key_path:
198
+ key_path = os.environ.get("SSL_KEY_PATH")
199
+
200
+ # Check default XDG paths if still not found
201
+ if not cert_path or not key_path:
202
+ default_cert, default_key = get_default_cert_paths()
203
+ if default_cert and default_key:
204
+ cert_path = str(default_cert)
205
+ key_path = str(default_key)
206
+
207
+ # Validate certificate files exist
208
+ use_ssl = False
209
+ if cert_path and key_path:
210
+ cert_exists = Path(cert_path).exists()
211
+ key_exists = Path(key_path).exists()
212
+ if cert_exists and key_exists:
213
+ use_ssl = True
214
+ _log.info("HTTPS enabled with certificates:")
215
+ _log.info(" Certificate: %s", cert_path)
216
+ _log.info(" Private key: %s", key_path)
217
+ else:
218
+ if not cert_exists:
219
+ _log.warning("SSL certificate not found: %s", cert_path)
220
+ if not key_exists:
221
+ _log.warning("SSL private key not found: %s", key_path)
222
+ _log.warning("Falling back to HTTP (insecure)")
223
+ else:
224
+ _log.warning(
225
+ "No SSL certificates configured. Running in HTTP mode (insecure). "
226
+ "For HTTPS, run: mkcert -install && mkcert -cert-file ~/.config/zebra_day/certs/server.crt "
227
+ "-key-file ~/.config/zebra_day/certs/server.key localhost 127.0.0.1 ::1"
228
+ )
229
+
230
+ # Build uvicorn config
231
+ uvicorn_kwargs = {
232
+ "host": host,
233
+ "port": port,
234
+ "reload": reload,
235
+ "factory": True,
236
+ }
237
+
238
+ if use_ssl:
239
+ uvicorn_kwargs["ssl_certfile"] = cert_path
240
+ uvicorn_kwargs["ssl_keyfile"] = key_path
241
+ protocol = "https"
242
+ else:
243
+ protocol = "http"
244
+
245
+ _log.info("Starting server at %s://%s:%d", protocol, host, port)
246
+
247
+ uvicorn.run("zebra_day.web.app:create_app", **uvicorn_kwargs)
171
248
 
@@ -39,17 +39,30 @@ class PrintResponse(BaseModel):
39
39
 
40
40
 
41
41
  class PrinterInfo(BaseModel):
42
- """Printer information model."""
43
- name: str
42
+ """Printer information model (v2.0.0 schema)."""
43
+ id: str = Field(..., description="Printer identifier/key in JSON")
44
44
  ip_address: str
45
+ printer_name: Optional[str] = Field(None, description="User-friendly display name")
46
+ lab_location: Optional[str] = Field(None, description="Location within the lab")
47
+ manufacturer: str = Field("zebra", description="Printer manufacturer")
45
48
  model: str
46
49
  serial: str
47
50
  label_zpl_styles: List[str]
51
+ default_label_style: Optional[str] = Field(None, description="Default label style to use when none specified")
48
52
  print_method: str
53
+ notes: Optional[str] = Field("", description="Optional notes")
54
+
55
+
56
+ class LabInfo(BaseModel):
57
+ """Lab information model (v2.0.0 schema)."""
58
+ id: str = Field(..., description="Lab identifier/key in JSON")
59
+ lab_name: str = Field(..., description="Human-readable lab name")
60
+ available_locations: List[str] = Field(default_factory=list, description="Valid location options for printers")
61
+ printers: List[PrinterInfo]
49
62
 
50
63
 
51
64
  class LabPrinters(BaseModel):
52
- """Lab and its printers."""
65
+ """Lab and its printers (deprecated, use LabInfo)."""
53
66
  lab: str
54
67
  printers: List[PrinterInfo]
55
68
 
@@ -63,6 +76,44 @@ async def list_labs(request: Request) -> List[str]:
63
76
  return list(zp.printers.get("labs", {}).keys())
64
77
 
65
78
 
79
+ @router.get("/labs/{lab}", response_model=LabInfo)
80
+ async def get_lab(request: Request, lab: str) -> LabInfo:
81
+ """Get lab details including available locations and printers."""
82
+ zp = request.app.state.zp
83
+ labs = zp.printers.get("labs", {})
84
+
85
+ if lab not in labs:
86
+ raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
87
+
88
+ lab_data = labs[lab]
89
+ lab_printers = lab_data.get("printers", {})
90
+
91
+ printers = []
92
+ for printer_id, info in lab_printers.items():
93
+ printers.append(
94
+ PrinterInfo(
95
+ id=printer_id,
96
+ ip_address=info.get("ip_address", ""),
97
+ printer_name=info.get("printer_name"),
98
+ lab_location=info.get("lab_location"),
99
+ manufacturer=info.get("manufacturer", "zebra"),
100
+ model=info.get("model", ""),
101
+ serial=info.get("serial", ""),
102
+ label_zpl_styles=info.get("label_zpl_styles", []),
103
+ default_label_style=info.get("default_label_style"),
104
+ print_method=info.get("print_method", ""),
105
+ notes=info.get("notes", ""),
106
+ )
107
+ )
108
+
109
+ return LabInfo(
110
+ id=lab,
111
+ lab_name=lab_data.get("lab_name", lab),
112
+ available_locations=lab_data.get("available_locations", []),
113
+ printers=printers,
114
+ )
115
+
116
+
66
117
  @router.get("/labs/{lab}/printers", response_model=List[PrinterInfo])
67
118
  async def list_printers(request: Request, lab: str) -> List[PrinterInfo]:
68
119
  """List all printers in a lab."""
@@ -72,16 +123,24 @@ async def list_printers(request: Request, lab: str) -> List[PrinterInfo]:
72
123
  if lab not in labs:
73
124
  raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
74
125
 
126
+ # Access printers via nested 'printers' key (v2 schema)
127
+ lab_printers = labs[lab].get("printers", {})
128
+
75
129
  printers = []
76
- for name, info in labs[lab].items():
130
+ for printer_id, info in lab_printers.items():
77
131
  printers.append(
78
132
  PrinterInfo(
79
- name=name,
133
+ id=printer_id,
80
134
  ip_address=info.get("ip_address", ""),
135
+ printer_name=info.get("printer_name"),
136
+ lab_location=info.get("lab_location"),
137
+ manufacturer=info.get("manufacturer", "zebra"),
81
138
  model=info.get("model", ""),
82
139
  serial=info.get("serial", ""),
83
140
  label_zpl_styles=info.get("label_zpl_styles", []),
141
+ default_label_style=info.get("default_label_style"),
84
142
  print_method=info.get("print_method", ""),
143
+ notes=info.get("notes", ""),
85
144
  )
86
145
  )
87
146
  return printers
@@ -161,3 +220,94 @@ async def get_config(request: Request) -> Dict[str, Any]:
161
220
  zp = request.app.state.zp
162
221
  return zp.printers
163
222
 
223
+
224
+ # ----- Lab Settings Endpoints -----
225
+
226
+ class LabUpdateRequest(BaseModel):
227
+ """Request model for updating lab settings."""
228
+ lab_name: Optional[str] = Field(None, description="Human-readable lab name")
229
+ available_locations: Optional[List[str]] = Field(None, description="List of valid locations")
230
+
231
+
232
+ class PrinterUpdateRequest(BaseModel):
233
+ """Request model for updating printer settings."""
234
+ printer_name: Optional[str] = Field(None, description="User-friendly display name")
235
+ lab_location: Optional[str] = Field(None, description="Location within the lab")
236
+ notes: Optional[str] = Field(None, description="Optional notes")
237
+ label_zpl_styles: Optional[List[str]] = Field(None, description="Allowed ZPL styles")
238
+ default_label_style: Optional[str] = Field(None, description="Default label style to use when none specified")
239
+
240
+
241
+ @router.patch("/labs/{lab}", response_model=LabInfo)
242
+ async def update_lab(request: Request, lab: str, update: LabUpdateRequest) -> LabInfo:
243
+ """Update lab settings (lab_name, available_locations)."""
244
+ zp = request.app.state.zp
245
+ labs = zp.printers.get("labs", {})
246
+
247
+ if lab not in labs:
248
+ raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
249
+
250
+ lab_data = labs[lab]
251
+
252
+ if update.lab_name is not None:
253
+ lab_data["lab_name"] = update.lab_name
254
+ if update.available_locations is not None:
255
+ lab_data["available_locations"] = update.available_locations
256
+
257
+ # Save changes
258
+ zp.save_printer_json(zp.printers_filename, relative=False)
259
+
260
+ # Return updated lab info
261
+ return await get_lab(request, lab)
262
+
263
+
264
+ @router.patch("/labs/{lab}/printers/{printer_id}")
265
+ async def update_printer(
266
+ request: Request, lab: str, printer_id: str, update: PrinterUpdateRequest
267
+ ) -> PrinterInfo:
268
+ """Update printer settings (printer_name, lab_location, notes)."""
269
+ zp = request.app.state.zp
270
+ labs = zp.printers.get("labs", {})
271
+
272
+ if lab not in labs:
273
+ raise HTTPException(status_code=404, detail=f"Lab '{lab}' not found")
274
+
275
+ lab_printers = labs[lab].get("printers", {})
276
+ if printer_id not in lab_printers:
277
+ raise HTTPException(status_code=404, detail=f"Printer '{printer_id}' not found in lab '{lab}'")
278
+
279
+ printer_data = lab_printers[printer_id]
280
+
281
+ if update.printer_name is not None:
282
+ printer_data["printer_name"] = update.printer_name if update.printer_name else None
283
+ if update.lab_location is not None:
284
+ printer_data["lab_location"] = update.lab_location if update.lab_location else None
285
+ if update.notes is not None:
286
+ printer_data["notes"] = update.notes
287
+ if update.label_zpl_styles is not None:
288
+ printer_data["label_zpl_styles"] = update.label_zpl_styles
289
+ if update.default_label_style is not None:
290
+ # Validate that the style exists in label_zpl_styles (if it's not empty string)
291
+ if update.default_label_style and update.default_label_style not in printer_data.get("label_zpl_styles", []):
292
+ raise HTTPException(
293
+ status_code=400,
294
+ detail=f"Default label style '{update.default_label_style}' must be one of: {printer_data.get('label_zpl_styles', [])}"
295
+ )
296
+ printer_data["default_label_style"] = update.default_label_style if update.default_label_style else None
297
+
298
+ # Save changes
299
+ zp.save_printer_json(zp.printers_filename, relative=False)
300
+
301
+ return PrinterInfo(
302
+ id=printer_id,
303
+ ip_address=printer_data.get("ip_address", ""),
304
+ printer_name=printer_data.get("printer_name"),
305
+ lab_location=printer_data.get("lab_location"),
306
+ manufacturer=printer_data.get("manufacturer", "zebra"),
307
+ model=printer_data.get("model", ""),
308
+ serial=printer_data.get("serial", ""),
309
+ label_zpl_styles=printer_data.get("label_zpl_styles", []),
310
+ default_label_style=printer_data.get("default_label_style"),
311
+ print_method=printer_data.get("print_method", ""),
312
+ notes=printer_data.get("notes", ""),
313
+ )