zebra-day 2.0.0__py3-none-any.whl → 2.1.4__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 (101) hide show
  1. zebra_day/__init__.py +7 -2
  2. zebra_day/_version.py +1 -0
  3. zebra_day/cli/__init__.py +80 -30
  4. zebra_day/cli/cognito.py +15 -9
  5. zebra_day/cli/gui.py +21 -16
  6. zebra_day/cli/printer.py +34 -27
  7. zebra_day/cli/template.py +19 -15
  8. zebra_day/cmd_mgr.py +3 -6
  9. zebra_day/docs/gx420d-gx430d-ug-en.pdf +0 -0
  10. zebra_day/docs/hardware_config_guide.md +149 -0
  11. zebra_day/docs/programatic_guide.md +181 -0
  12. zebra_day/docs/qln420_zebra_manual.pdf +0 -0
  13. zebra_day/docs/uid_screed_light.md +38 -0
  14. zebra_day/docs/zd620-zd420-ug-en.pdf +0 -0
  15. zebra_day/docs/zebra_day_ui_guide.md +194 -0
  16. zebra_day/etc/printer_config.json +7 -1
  17. zebra_day/etc/printer_config.template.json +3 -17
  18. zebra_day/etc/tmp_printers139.json +10 -0
  19. zebra_day/etc/tmp_printers147.json +10 -0
  20. zebra_day/etc/tmp_printers34.json +10 -0
  21. zebra_day/etc/tmp_printers389.json +10 -0
  22. zebra_day/etc/tmp_printers398.json +10 -0
  23. zebra_day/etc/tmp_printers437.json +10 -0
  24. zebra_day/etc/tmp_printers439.json +10 -0
  25. zebra_day/etc/tmp_printers440.json +10 -0
  26. zebra_day/etc/tmp_printers508.json +10 -0
  27. zebra_day/etc/tmp_printers543.json +10 -0
  28. zebra_day/etc/tmp_printers835.json +10 -0
  29. zebra_day/etc/tmp_printers842.json +10 -0
  30. zebra_day/etc/tmp_printers931.json +10 -0
  31. zebra_day/etc/tmp_printers969.json +10 -0
  32. zebra_day/exceptions.py +1 -1
  33. zebra_day/files/corners_smallTube_preview.png +0 -0
  34. zebra_day/files/test_png_2897.png +0 -0
  35. zebra_day/files/test_png_31690.png +0 -0
  36. zebra_day/files/test_png_33804.png +0 -0
  37. zebra_day/files/test_png_34737.png +0 -0
  38. zebra_day/files/test_png_4161.png +0 -0
  39. zebra_day/files/test_png_44748.png +0 -0
  40. zebra_day/files/test_png_4635.png +0 -0
  41. zebra_day/files/test_png_56349.png +0 -0
  42. zebra_day/files/test_png_5936.png +0 -0
  43. zebra_day/files/test_png_64110.png +0 -0
  44. zebra_day/files/test_png_64891.png +0 -0
  45. zebra_day/files/test_png_69002.png +0 -0
  46. zebra_day/files/test_png_70065.png +0 -0
  47. zebra_day/files/test_png_72366.png +0 -0
  48. zebra_day/files/test_png_77793.png +0 -0
  49. zebra_day/files/test_png_9572.png +0 -0
  50. zebra_day/imgs/.hold +0 -0
  51. zebra_day/imgs/bar_ltpurp.png +0 -0
  52. zebra_day/imgs/bar_purp.png +0 -0
  53. zebra_day/imgs/bar_purp3.png +0 -0
  54. zebra_day/imgs/bar_red.png +0 -0
  55. zebra_day/imgs/legacy/UBC_gantt_chart.png +0 -0
  56. zebra_day/imgs/legacy/gx420d_network_config.png +0 -0
  57. zebra_day/imgs/legacy/gx420d_printer_config.png +0 -0
  58. zebra_day/imgs/legacy/ngrok.png +0 -0
  59. zebra_day/imgs/legacy/printer_details.png +0 -0
  60. zebra_day/imgs/legacy/quick_start_test_label.png +0 -0
  61. zebra_day/imgs/legacy/quick_start_test_label2.png +0 -0
  62. zebra_day/imgs/legacy/zd620_network_config.png +0 -0
  63. zebra_day/imgs/legacy/zd620_printer_config.png +0 -0
  64. zebra_day/imgs/legacy/zday_quick_gui.png +0 -0
  65. zebra_day/imgs/legacy/zebra_day_alt_css_dog.png +0 -0
  66. zebra_day/imgs/legacy/zebra_day_alt_css_flower.png +0 -0
  67. zebra_day/imgs/legacy/zebra_day_alt_css_main.png +0 -0
  68. zebra_day/imgs/legacy/zebra_day_available_zpl_templates.png +0 -0
  69. zebra_day/imgs/legacy/zebra_day_bkup_pconfig.png +0 -0
  70. zebra_day/imgs/legacy/zebra_day_home.png +0 -0
  71. zebra_day/imgs/legacy/zebra_day_manual_print.png +0 -0
  72. zebra_day/imgs/legacy/zebra_day_printer_fleet_json.png +0 -0
  73. zebra_day/imgs/legacy/zebra_day_quick_ex.png +0 -0
  74. zebra_day/imgs/legacy/zebra_day_zpl_template_IRLa.png +0 -0
  75. zebra_day/imgs/legacy/zebra_day_zpl_template_IRLb.png +0 -0
  76. zebra_day/imgs/ui_api_docs.png +0 -0
  77. zebra_day/imgs/ui_config.png +0 -0
  78. zebra_day/imgs/ui_dashboard.png +0 -0
  79. zebra_day/imgs/ui_print_request.png +0 -0
  80. zebra_day/imgs/ui_printers.png +0 -0
  81. zebra_day/imgs/ui_templates.png +0 -0
  82. zebra_day/logging_config.py +4 -9
  83. zebra_day/mkcert.py +157 -0
  84. zebra_day/paths.py +1 -2
  85. zebra_day/print_mgr.py +165 -145
  86. zebra_day/templates/modern/config.html +7 -0
  87. zebra_day/templates/modern/print_request.html +61 -3
  88. zebra_day/web/__init__.py +1 -1
  89. zebra_day/web/app.py +21 -16
  90. zebra_day/web/auth.py +17 -15
  91. zebra_day/web/middleware.py +8 -5
  92. zebra_day/web/routers/__init__.py +0 -1
  93. zebra_day/web/routers/api.py +192 -43
  94. zebra_day/web/routers/ui.py +31 -33
  95. zebra_day/zpl_renderer.py +45 -34
  96. {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/METADATA +76 -67
  97. {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/RECORD +101 -29
  98. {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/WHEEL +0 -0
  99. {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/entry_points.txt +0 -0
  100. {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/licenses/LICENSE +0 -0
  101. {zebra_day-2.0.0.dist-info → zebra_day-2.1.4.dist-info}/top_level.txt +0 -0
@@ -4,23 +4,30 @@ Versioned JSON API router for zebra_day.
4
4
  Provides programmatic access to printer management and label printing.
5
5
  All endpoints return JSON and are prefixed with /api/v1/.
6
6
  """
7
+
7
8
  from __future__ import annotations
8
9
 
9
- from typing import Optional, List, Dict, Any
10
+ from datetime import datetime
11
+ from typing import Any
10
12
 
11
13
  from fastapi import APIRouter, HTTPException, Request
14
+ from fastapi.responses import FileResponse
12
15
  from pydantic import BaseModel, Field
13
16
 
17
+ from zebra_day import paths as xdg
18
+
14
19
  router = APIRouter()
15
20
 
16
21
 
17
22
  # ----- Request/Response Models -----
18
23
 
24
+
19
25
  class PrintRequest(BaseModel):
20
26
  """Request model for printing a label."""
27
+
21
28
  lab: str = Field(..., description="Lab identifier")
22
29
  printer: str = Field(..., description="Printer name")
23
- label_zpl_style: Optional[str] = Field(None, description="ZPL template name")
30
+ label_zpl_style: str | None = Field(None, description="ZPL template name")
24
31
  uid_barcode: str = Field("", description="UID for barcode")
25
32
  alt_a: str = Field("", description="Alternative field A")
26
33
  alt_b: str = Field("", description="Alternative field B")
@@ -33,44 +40,77 @@ class PrintRequest(BaseModel):
33
40
 
34
41
  class PrintResponse(BaseModel):
35
42
  """Response model for print request."""
43
+
36
44
  success: bool
37
45
  message: str
38
- png_url: Optional[str] = None
46
+ png_url: str | None = None
39
47
 
40
48
 
41
49
  class PrinterInfo(BaseModel):
42
50
  """Printer information model (v2.0.0 schema)."""
51
+
43
52
  id: str = Field(..., description="Printer identifier/key in JSON")
44
53
  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")
54
+ printer_name: str | None = Field(None, description="User-friendly display name")
55
+ lab_location: str | None = Field(None, description="Location within the lab")
47
56
  manufacturer: str = Field("zebra", description="Printer manufacturer")
48
57
  model: str
49
58
  serial: str
50
- label_zpl_styles: List[str]
51
- default_label_style: Optional[str] = Field(None, description="Default label style to use when none specified")
59
+ label_zpl_styles: list[str]
60
+ default_label_style: str | None = Field(
61
+ None, description="Default label style to use when none specified"
62
+ )
52
63
  print_method: str
53
- notes: Optional[str] = Field("", description="Optional notes")
64
+ notes: str | None = Field("", description="Optional notes")
54
65
 
55
66
 
56
67
  class LabInfo(BaseModel):
57
68
  """Lab information model (v2.0.0 schema)."""
69
+
58
70
  id: str = Field(..., description="Lab identifier/key in JSON")
59
71
  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]
72
+ available_locations: list[str] = Field(
73
+ default_factory=list, description="Valid location options for printers"
74
+ )
75
+ printers: list[PrinterInfo]
62
76
 
63
77
 
64
78
  class LabPrinters(BaseModel):
65
79
  """Lab and its printers (deprecated, use LabInfo)."""
80
+
66
81
  lab: str
67
- printers: List[PrinterInfo]
82
+ printers: list[PrinterInfo]
83
+
84
+
85
+ class RenderRequest(BaseModel):
86
+ """Request model for rendering ZPL to PNG."""
87
+
88
+ template: str | None = Field(None, description="ZPL template name (e.g., 'tube_2inX1in')")
89
+ zpl_content: str | None = Field(
90
+ None, description="Raw ZPL content (takes precedence over template)"
91
+ )
92
+ uid_barcode: str = Field("", description="UID for barcode")
93
+ alt_a: str = Field("", description="Alternative field A")
94
+ alt_b: str = Field("", description="Alternative field B")
95
+ alt_c: str = Field("", description="Alternative field C")
96
+ alt_d: str = Field("", description="Alternative field D")
97
+ alt_e: str = Field("", description="Alternative field E")
98
+ alt_f: str = Field("", description="Alternative field F")
99
+
100
+
101
+ class RenderResponse(BaseModel):
102
+ """Response model for render request (when not returning PNG directly)."""
103
+
104
+ success: bool
105
+ message: str
106
+ png_url: str = Field(..., description="URL to download the generated PNG")
68
107
 
69
108
 
70
109
  # ----- Endpoints -----
71
110
 
72
- @router.get("/labs", response_model=List[str])
73
- async def list_labs(request: Request) -> List[str]:
111
+
112
+ @router.get("/labs", response_model=list[str])
113
+ async def list_labs(request: Request) -> list[str]:
74
114
  """List all available labs."""
75
115
  zp = request.app.state.zp
76
116
  return list(zp.printers.get("labs", {}).keys())
@@ -114,8 +154,8 @@ async def get_lab(request: Request, lab: str) -> LabInfo:
114
154
  )
115
155
 
116
156
 
117
- @router.get("/labs/{lab}/printers", response_model=List[PrinterInfo])
118
- async def list_printers(request: Request, lab: str) -> List[PrinterInfo]:
157
+ @router.get("/labs/{lab}/printers", response_model=list[PrinterInfo])
158
+ async def list_printers(request: Request, lab: str) -> list[PrinterInfo]:
119
159
  """List all printers in a lab."""
120
160
  zp = request.app.state.zp
121
161
  labs = zp.printers.get("labs", {})
@@ -146,10 +186,9 @@ async def list_printers(request: Request, lab: str) -> List[PrinterInfo]:
146
186
  return printers
147
187
 
148
188
 
149
- @router.get("/templates", response_model=List[str])
150
- async def list_templates(request: Request) -> List[str]:
189
+ @router.get("/templates", response_model=list[str])
190
+ async def list_templates(request: Request) -> list[str]:
151
191
  """List all available ZPL templates."""
152
- from pathlib import Path
153
192
 
154
193
  pkg_path = request.app.state.pkg_path
155
194
  styles_dir = pkg_path / "etc" / "label_styles"
@@ -183,7 +222,7 @@ async def print_label(request: Request, print_req: PrintRequest) -> PrintRespons
183
222
  raise HTTPException(status_code=429, detail=reason)
184
223
 
185
224
  try:
186
- result = zp.print_zpl(
225
+ zp.print_zpl(
187
226
  lab=print_req.lab,
188
227
  printer_name=print_req.printer,
189
228
  label_zpl_style=print_req.label_zpl_style,
@@ -197,45 +236,149 @@ async def print_label(request: Request, print_req: PrintRequest) -> PrintRespons
197
236
  print_n=print_req.copies,
198
237
  client_ip=client_ip,
199
238
  )
200
-
201
- # Check if result is a PNG file path
202
- if result and ".png" in str(result):
203
- png_name = str(result).split("/")[-1]
204
- return PrintResponse(
205
- success=True,
206
- message="PNG generated successfully",
207
- png_url=f"/files/{png_name}",
208
- )
209
-
210
239
  return PrintResponse(success=True, message="Print request sent successfully")
211
240
  except Exception as e:
212
- raise HTTPException(status_code=500, detail=str(e))
241
+ raise HTTPException(status_code=500, detail=str(e)) from None
213
242
  finally:
214
243
  rate_limiter.release()
215
244
 
216
245
 
246
+ @router.post("/render", response_model=RenderResponse)
247
+ async def render_label(request: Request, render_req: RenderRequest) -> RenderResponse:
248
+ """
249
+ Render ZPL to PNG image.
250
+
251
+ This endpoint generates a PNG image from ZPL content without sending to a printer.
252
+ You can provide either:
253
+ - A template name (e.g., 'tube_2inX1in') with field values
254
+ - Raw ZPL content directly
255
+
256
+ Returns a URL to download the generated PNG.
257
+ """
258
+ zp = request.app.state.zp
259
+
260
+ # Validate that we have either template or zpl_content
261
+ if not render_req.template and not render_req.zpl_content:
262
+ raise HTTPException(
263
+ status_code=400, detail="Either 'template' or 'zpl_content' must be provided"
264
+ )
265
+
266
+ try:
267
+ # Generate ZPL string from template if not provided directly
268
+ if render_req.zpl_content:
269
+ zpl_string = render_req.zpl_content
270
+ else:
271
+ zpl_string = zp.formulate_zpl(
272
+ uid_barcode=render_req.uid_barcode,
273
+ alt_a=render_req.alt_a,
274
+ alt_b=render_req.alt_b,
275
+ alt_c=render_req.alt_c,
276
+ alt_d=render_req.alt_d,
277
+ alt_e=render_req.alt_e,
278
+ alt_f=render_req.alt_f,
279
+ label_zpl_style=render_req.template,
280
+ )
281
+
282
+ # Generate unique filename
283
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S.%f")
284
+ template_name = render_req.template or "custom"
285
+ png_filename = f"zpl_render_{template_name}_{timestamp}.png"
286
+ png_path = xdg.get_generated_files_dir() / png_filename
287
+
288
+ # Render to PNG
289
+ zp.generate_label_png(zpl_string, str(png_path), relative=False)
290
+
291
+ return RenderResponse(
292
+ success=True,
293
+ message="PNG rendered successfully",
294
+ png_url=f"/generated/{png_filename}",
295
+ )
296
+ except FileNotFoundError as e:
297
+ raise HTTPException(status_code=404, detail=f"Template not found: {e}") from None
298
+ except Exception as e:
299
+ raise HTTPException(status_code=500, detail=f"Render failed: {e}") from None
300
+
301
+
302
+ @router.post("/render/png")
303
+ async def render_label_png(request: Request, render_req: RenderRequest):
304
+ """
305
+ Render ZPL to PNG and return the image directly.
306
+
307
+ Same as /render but returns the PNG file directly instead of a URL.
308
+ Useful for programmatic access where you want the image bytes.
309
+ """
310
+ zp = request.app.state.zp
311
+
312
+ # Validate that we have either template or zpl_content
313
+ if not render_req.template and not render_req.zpl_content:
314
+ raise HTTPException(
315
+ status_code=400, detail="Either 'template' or 'zpl_content' must be provided"
316
+ )
317
+
318
+ try:
319
+ # Generate ZPL string from template if not provided directly
320
+ if render_req.zpl_content:
321
+ zpl_string = render_req.zpl_content
322
+ else:
323
+ zpl_string = zp.formulate_zpl(
324
+ uid_barcode=render_req.uid_barcode,
325
+ alt_a=render_req.alt_a,
326
+ alt_b=render_req.alt_b,
327
+ alt_c=render_req.alt_c,
328
+ alt_d=render_req.alt_d,
329
+ alt_e=render_req.alt_e,
330
+ alt_f=render_req.alt_f,
331
+ label_zpl_style=render_req.template,
332
+ )
333
+
334
+ # Generate unique filename
335
+ timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S.%f")
336
+ template_name = render_req.template or "custom"
337
+ png_filename = f"zpl_render_{template_name}_{timestamp}.png"
338
+ png_path = xdg.get_generated_files_dir() / png_filename
339
+
340
+ # Render to PNG
341
+ zp.generate_label_png(zpl_string, str(png_path), relative=False)
342
+
343
+ # Return the file directly
344
+ return FileResponse(
345
+ path=str(png_path),
346
+ media_type="image/png",
347
+ filename=png_filename,
348
+ )
349
+ except FileNotFoundError as e:
350
+ raise HTTPException(status_code=404, detail=f"Template not found: {e}") from None
351
+ except Exception as e:
352
+ raise HTTPException(status_code=500, detail=f"Render failed: {e}") from None
353
+
354
+
217
355
  @router.get("/config")
218
- async def get_config(request: Request) -> Dict[str, Any]:
356
+ async def get_config(request: Request) -> dict[str, Any]:
219
357
  """Get the current printer configuration."""
220
358
  zp = request.app.state.zp
221
- return zp.printers
359
+ return dict(zp.printers)
222
360
 
223
361
 
224
362
  # ----- Lab Settings Endpoints -----
225
363
 
364
+
226
365
  class LabUpdateRequest(BaseModel):
227
366
  """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")
367
+
368
+ lab_name: str | None = Field(None, description="Human-readable lab name")
369
+ available_locations: list[str] | None = Field(None, description="List of valid locations")
230
370
 
231
371
 
232
372
  class PrinterUpdateRequest(BaseModel):
233
373
  """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")
374
+
375
+ printer_name: str | None = Field(None, description="User-friendly display name")
376
+ lab_location: str | None = Field(None, description="Location within the lab")
377
+ notes: str | None = Field(None, description="Optional notes")
378
+ label_zpl_styles: list[str] | None = Field(None, description="Allowed ZPL styles")
379
+ default_label_style: str | None = Field(
380
+ None, description="Default label style to use when none specified"
381
+ )
239
382
 
240
383
 
241
384
  @router.patch("/labs/{lab}", response_model=LabInfo)
@@ -274,7 +417,9 @@ async def update_printer(
274
417
 
275
418
  lab_printers = labs[lab].get("printers", {})
276
419
  if printer_id not in lab_printers:
277
- raise HTTPException(status_code=404, detail=f"Printer '{printer_id}' not found in lab '{lab}'")
420
+ raise HTTPException(
421
+ status_code=404, detail=f"Printer '{printer_id}' not found in lab '{lab}'"
422
+ )
278
423
 
279
424
  printer_data = lab_printers[printer_id]
280
425
 
@@ -288,12 +433,16 @@ async def update_printer(
288
433
  printer_data["label_zpl_styles"] = update.label_zpl_styles
289
434
  if update.default_label_style is not None:
290
435
  # 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", []):
436
+ if update.default_label_style and update.default_label_style not in printer_data.get(
437
+ "label_zpl_styles", []
438
+ ):
292
439
  raise HTTPException(
293
440
  status_code=400,
294
- detail=f"Default label style '{update.default_label_style}' must be one of: {printer_data.get('label_zpl_styles', [])}"
441
+ detail=f"Default label style '{update.default_label_style}' must be one of: {printer_data.get('label_zpl_styles', [])}",
295
442
  )
296
- printer_data["default_label_style"] = update.default_label_style if update.default_label_style else None
443
+ printer_data["default_label_style"] = (
444
+ update.default_label_style if update.default_label_style else None
445
+ )
297
446
 
298
447
  # Save changes
299
448
  zp.save_printer_json(zp.printers_filename, relative=False)
@@ -4,22 +4,20 @@ UI router for zebra_day web interface.
4
4
  Provides HTML endpoints for the web-based management interface.
5
5
  All routes use the modern UI design with responsive layouts.
6
6
  """
7
+
7
8
  from __future__ import annotations
8
9
 
9
10
  import json
10
- import os
11
- import subprocess
12
11
  import tempfile
13
12
  import time
14
13
  from datetime import datetime
15
14
  from pathlib import Path
16
- from typing import Optional
17
15
 
18
- from fastapi import APIRouter, Request, Form, HTTPException
16
+ from fastapi import APIRouter, Form, HTTPException, Request
19
17
  from fastapi.responses import HTMLResponse, RedirectResponse, Response
20
18
 
21
- from zebra_day.logging_config import get_logger
22
19
  import zebra_day.cmd_mgr as zdcm
20
+ from zebra_day.logging_config import get_logger
23
21
 
24
22
  _log = get_logger(__name__)
25
23
 
@@ -92,6 +90,7 @@ def get_stats(zp, pkg_path: Path) -> dict:
92
90
  # MODERN UI ROUTES (root level)
93
91
  # =============================================================================
94
92
 
93
+
95
94
  @router.get("/", response_class=HTMLResponse)
96
95
  async def modern_dashboard(request: Request):
97
96
  """Modern dashboard - home page."""
@@ -145,19 +144,21 @@ async def modern_printers_by_lab(request: Request, lab: str):
145
144
 
146
145
  printers = []
147
146
  for name, info in lab_printers.items():
148
- printers.append({
149
- "id": name,
150
- "name": info.get("printer_name") or name, # Display name or fallback to ID
151
- "printer_name": info.get("printer_name"),
152
- "ip_address": info.get("ip_address", ""),
153
- "lab_location": info.get("lab_location"),
154
- "manufacturer": info.get("manufacturer", "zebra"),
155
- "model": info.get("model", ""),
156
- "serial": info.get("serial", ""),
157
- "label_zpl_styles": info.get("label_zpl_styles", []),
158
- "status": "online" if info.get("ip_address") else "unknown",
159
- "notes": info.get("notes", ""),
160
- })
147
+ printers.append(
148
+ {
149
+ "id": name,
150
+ "name": info.get("printer_name") or name, # Display name or fallback to ID
151
+ "printer_name": info.get("printer_name"),
152
+ "ip_address": info.get("ip_address", ""),
153
+ "lab_location": info.get("lab_location"),
154
+ "manufacturer": info.get("manufacturer", "zebra"),
155
+ "model": info.get("model", ""),
156
+ "serial": info.get("serial", ""),
157
+ "label_zpl_styles": info.get("label_zpl_styles", []),
158
+ "status": "online" if info.get("ip_address") else "unknown",
159
+ "notes": info.get("notes", ""),
160
+ }
161
+ )
161
162
 
162
163
  ip_root = ".".join(request.app.state.local_ip.split(".")[:-1])
163
164
 
@@ -332,13 +333,13 @@ async def modern_template_preview(
332
333
  template_name = filepath.stem
333
334
  output_path = output_dir / f"{template_name}_preview.png"
334
335
 
335
- result = zp.generate_label_png(zpl_content, str(output_path), False)
336
+ zp.generate_label_png(zpl_content, str(output_path), False)
336
337
 
337
338
  # Return redirect to the generated file
338
339
  return RedirectResponse(url=f"/files/{template_name}_preview.png", status_code=303)
339
340
 
340
341
  except Exception as e:
341
- raise HTTPException(status_code=500, detail=f"Preview generation failed: {e}")
342
+ raise HTTPException(status_code=500, detail=f"Preview generation failed: {e}") from None
342
343
 
343
344
 
344
345
  @router.get("/config", response_class=HTMLResponse)
@@ -360,12 +361,16 @@ async def modern_config(request: Request):
360
361
  "backups": stats["backups"],
361
362
  }
362
363
 
364
+ # Get the config file path that was loaded
365
+ config_file_path = getattr(zp, "printers_filename", "Unknown")
366
+
363
367
  context = get_modern_context(
364
368
  request,
365
369
  active_page="config",
366
370
  labs=labs,
367
371
  ip_root=ip_root,
368
372
  config_summary=config_summary,
373
+ config_file_path=config_file_path,
369
374
  )
370
375
  return templates.TemplateResponse("modern/config.html", context)
371
376
 
@@ -388,7 +393,7 @@ async def modern_config_view(request: Request):
388
393
 
389
394
 
390
395
  @router.get("/config/edit", response_class=HTMLResponse)
391
- async def modern_config_edit(request: Request, error_msg: Optional[str] = None):
396
+ async def modern_config_edit(request: Request, error_msg: str | None = None):
392
397
  """Edit printer configuration JSON."""
393
398
  zp = request.app.state.zp
394
399
  templates = request.app.state.templates
@@ -433,10 +438,7 @@ async def modern_config_save(request: Request, json_data: str = Form(...)):
433
438
  return RedirectResponse(url="/config", status_code=303)
434
439
 
435
440
  except json.JSONDecodeError as e:
436
- return RedirectResponse(
437
- url=f"/config/edit?error_msg=Invalid JSON: {e}",
438
- status_code=303
439
- )
441
+ return RedirectResponse(url=f"/config/edit?error_msg=Invalid JSON: {e}", status_code=303)
440
442
 
441
443
 
442
444
  @router.get("/config/backups", response_class=HTMLResponse)
@@ -504,9 +506,7 @@ async def modern_config_scan(
504
506
  ):
505
507
  """Scan network for printers."""
506
508
  zp = request.app.state.zp
507
- zp.probe_zebra_printers_add_to_printers_json(
508
- ip_stub=ip_stub, scan_wait=scan_wait, lab=lab
509
- )
509
+ zp.probe_zebra_printers_add_to_printers_json(ip_stub=ip_stub, scan_wait=scan_wait, lab=lab)
510
510
  time.sleep(2.2)
511
511
  return RedirectResponse(url=f"/printers/{lab}", status_code=303)
512
512
 
@@ -514,7 +514,7 @@ async def modern_config_scan(
514
514
  @router.get("/_print_label", response_class=HTMLResponse)
515
515
  async def modern_print_label(
516
516
  request: Request,
517
- lab: Optional[str] = None,
517
+ lab: str | None = None,
518
518
  printer: str = "",
519
519
  printer_ip: str = "",
520
520
  label_zpl_style: str = "",
@@ -565,7 +565,7 @@ async def modern_print_label(
565
565
  png_url = None
566
566
  if result and ".png" in str(result):
567
567
  png_name = str(result).split("/")[-1]
568
- png_url = f"/files/{png_name}"
568
+ png_url = f"/generated/{png_name}"
569
569
 
570
570
  context = get_modern_context(
571
571
  request,
@@ -623,9 +623,7 @@ async def modern_png_renderer(
623
623
  files_dir = pkg_path / "files"
624
624
  files_dir.mkdir(parents=True, exist_ok=True)
625
625
 
626
- png_tmp_f = tempfile.NamedTemporaryFile(
627
- suffix=".png", dir=str(files_dir), delete=False
628
- ).name
626
+ png_tmp_f = tempfile.NamedTemporaryFile(suffix=".png", dir=str(files_dir), delete=False).name
629
627
 
630
628
  zp.generate_label_png(content, png_fn=png_tmp_f)
631
629