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
zebra_day/print_mgr.py CHANGED
@@ -25,6 +25,7 @@ from importlib.resources import files
25
25
 
26
26
  from zebra_day.logging_config import get_logger
27
27
  from zebra_day import paths as xdg
28
+ import zebra_day.cmd_mgr as zdcm
28
29
 
29
30
  _log = get_logger(__name__)
30
31
 
@@ -123,9 +124,10 @@ class zpl:
123
124
  self.create_new_printers_json_with_single_test_printer(str(jcfg))
124
125
 
125
126
 
126
- def probe_zebra_printers_add_to_printers_json(self, ip_stub="192.168.1", scan_wait="0.25",lab="scan-results", relative=False):
127
+ def probe_zebra_printers_add_to_printers_json(self, ip_stub="192.168.1", scan_wait="0.25", lab="scan-results", relative=False):
127
128
  """
128
- Scan the network for zebra printers
129
+ Scan the network for zebra printers.
130
+
129
131
  NOTE! this should work with no dependencies on a MAC
130
132
  UBUNTU requires system wide net-tools (for arp)
131
133
  Others... well, this may not work
@@ -133,61 +135,106 @@ class zpl:
133
135
  ---
134
136
  Requires:
135
137
  curl is pretty standard, arp seems less so
136
- arp
138
+ arp
137
139
  ---
138
-
139
- ip_stub = all 255 possibilities will be probed beneath this
140
- stub provided
141
-
142
- can_wait = seconds to re-try probing until moving on. 0.25
143
- default may be too squick
144
140
 
145
- lab = code for the lab key to add/update to given finding
146
- new printers. Existing printers will be over written.
141
+ ip_stub = all 255 possibilities will be probed beneath this stub provided
142
+ scan_wait = seconds to re-try probing until moving on. 0.25 default may be too quick
143
+ lab = code for the lab key to add/update to given finding new printers
147
144
  """
145
+ # Ensure schema version is set
146
+ if "schema_version" not in self.printers:
147
+ self.printers["schema_version"] = "2.0.0"
148
148
 
149
+ # Initialize lab with v2 structure if not exists
149
150
  if lab not in self.printers['labs']:
150
- self.printers['labs'][lab] = {}
151
-
152
- self.printers['labs'][lab]["Download-Label-png"] = {
151
+ self.printers['labs'][lab] = {
152
+ "lab_name": lab,
153
+ "available_locations": [],
154
+ "printers": {}
155
+ }
156
+
157
+ # Ensure lab has printers sub-object (migration from v1)
158
+ if "printers" not in self.printers['labs'][lab]:
159
+ self.printers['labs'][lab]["printers"] = {}
160
+ self.printers['labs'][lab].setdefault("lab_name", lab)
161
+ self.printers['labs'][lab].setdefault("available_locations", [])
162
+
163
+ # Add the virtual PNG printer
164
+ self.printers['labs'][lab]["printers"]["Download-Label-png"] = {
153
165
  "ip_address": "dl_png",
154
- "label_zpl_styles": ["tube_2inX1in"],
155
- "print_method": "generate png",
166
+ "printer_name": "Download Label as PNG",
167
+ "lab_location": None,
168
+ "manufacturer": "virtual",
156
169
  "model": "na",
157
170
  "serial": "na",
158
- "arp_data": ""
171
+ "label_zpl_styles": ["tube_2inX1in"],
172
+ "default_label_style": "tube_2inX1in",
173
+ "print_method": "generate png",
174
+ "arp_data": "",
175
+ "notes": ""
159
176
  }
160
177
 
161
- # Run scanner script using subprocess instead of os.popen
162
- script_path = Path(str(files('zebra_day'))) / "bin" / "scan_for_networed_zebra_printers_curl.sh"
163
- result = subprocess.run(
164
- [str(script_path), ip_stub, scan_wait],
165
- capture_output=True,
166
- text=True,
167
- check=False
168
- )
169
-
170
- for line in result.stdout.splitlines():
171
- line = line.rstrip()
172
- sl = line.split('|')
173
- if len(sl) > 1:
174
- zp = sl[0]
175
- ip = sl[1]
176
- model = sl[2]
177
- serial = sl[3]
178
- status = sl[4]
179
- arp_response = sl[5]
180
-
181
- if ip not in self.printers['labs'][lab]:
182
- # The label formats set here are the installed defaults
183
- self.printers['labs'][lab][ip] = {
184
- "ip_address": ip,
185
- "label_zpl_styles": ["tube_2inX1in", "plate_1inX0.25in", "tube_2inX0.3in"],
186
- "print_method": "unk",
187
- "model": model,
188
- "serial": serial,
189
- "arp_data": arp_response
190
- }
178
+ # Scan network for Zebra printers using pure Python
179
+ wait_time = float(scan_wait) if scan_wait else 0.25
180
+
181
+ for i in range(1, 255):
182
+ ip = f"{ip_stub}.{i}"
183
+ try:
184
+ # Try to connect to ZPL port (9100)
185
+ import socket
186
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
187
+ sock.settimeout(wait_time)
188
+ result = sock.connect_ex((ip, 9100))
189
+ sock.close()
190
+
191
+ if result == 0:
192
+ # Port is open, try to get printer info
193
+ model = "Unknown"
194
+ serial = "Unknown"
195
+
196
+ try:
197
+ # Query printer for model and serial
198
+ printer = zdcm.ZebraPrinter(ip)
199
+ config = printer.get_configuration()
200
+
201
+ # Parse model from config
202
+ if "MODEL" in config:
203
+ for line in config.split('\n'):
204
+ if "MODEL" in line.upper():
205
+ parts = line.split(':')
206
+ if len(parts) > 1:
207
+ model = parts[1].strip()
208
+ break
209
+
210
+ # Parse serial from config
211
+ if "SERIAL" in config.upper():
212
+ for line in config.split('\n'):
213
+ if "SERIAL" in line.upper():
214
+ parts = line.split(':')
215
+ if len(parts) > 1:
216
+ serial = parts[1].strip()
217
+ break
218
+ except Exception:
219
+ pass # Use defaults if we can't query printer
220
+
221
+ if ip not in self.printers['labs'][lab]["printers"]:
222
+ # The label formats set here are the installed defaults
223
+ self.printers['labs'][lab]["printers"][ip] = {
224
+ "ip_address": ip,
225
+ "printer_name": None, # User can set friendly name later
226
+ "lab_location": None, # User can set location later
227
+ "manufacturer": "zebra",
228
+ "model": model,
229
+ "serial": serial,
230
+ "label_zpl_styles": ["tube_2inX1in", "plate_1inX0.25in", "tube_2inX0.3in"],
231
+ "default_label_style": "tube_2inX1in", # Default to first style
232
+ "print_method": "socket",
233
+ "arp_data": "",
234
+ "notes": ""
235
+ }
236
+ except Exception:
237
+ pass # Skip unreachable IPs
191
238
 
192
239
  self.save_printer_json(self.printers_filename, relative=False)
193
240
 
@@ -274,15 +321,18 @@ class zpl:
274
321
 
275
322
  def clear_printers_json(self, json_file: str = "/etc/printer_config.json") -> None:
276
323
  """
277
- Reset printers JSON to empty minimal structure.
324
+ Reset printers JSON to empty minimal v2.0.0 structure.
278
325
 
279
326
  Args:
280
327
  json_file: Path to the config file (relative to package)
281
328
  """
282
329
  json_path = Path(str(files('zebra_day'))) / json_file.lstrip('/')
283
330
 
284
- # Write empty config using pathlib
285
- empty_config = {"labs": {}}
331
+ # Write empty config with v2 schema
332
+ empty_config = {
333
+ "schema_version": "2.0.0",
334
+ "labs": {}
335
+ }
286
336
  json_path.parent.mkdir(parents=True, exist_ok=True)
287
337
  with open(json_path, 'w') as f:
288
338
  json.dump(empty_config, f, indent=4)
@@ -313,22 +363,24 @@ class zpl:
313
363
 
314
364
 
315
365
 
316
- def get_valid_label_styles_for_lab(self,lab=None):
366
+ def get_valid_label_styles_for_lab(self, lab=None):
317
367
  """
318
- The intention for this method was to confirm a template
319
- being requested for use in printing to some printer
320
- was 'allowed' by checking with that printers printer json
321
- for the array of valid templates.
368
+ Get all unique label styles available for printers in a lab.
322
369
 
323
- This was a huge PITA in testing, could be re-enabled at some point
370
+ The intention for this method was to confirm a template
371
+ being requested for use in printing to some printer
372
+ was 'allowed' by checking with that printers printer json
373
+ for the array of valid templates.
324
374
 
375
+ This was a huge PITA in testing, could be re-enabled at some point.
325
376
  It is used once, but prints a warning only.
326
377
  """
327
-
328
378
  unique_labels = set()
329
379
 
330
- for printer in self.printers['labs'][lab]:
331
- for style in self.printers['labs'][lab][printer]['label_zpl_styles']:
380
+ # Access printers via nested 'printers' key (v2 schema)
381
+ lab_printers = self.printers['labs'][lab].get('printers', {})
382
+ for printer_id, printer_data in lab_printers.items():
383
+ for style in printer_data.get('label_zpl_styles', []):
332
384
  unique_labels.add(style)
333
385
 
334
386
  result = list(unique_labels)
@@ -410,39 +462,43 @@ class zpl:
410
462
  def print_zpl(self, lab=None, printer_name=None, uid_barcode='', alt_a='', alt_b='', alt_c='', alt_d='', alt_e='', alt_f='', label_zpl_style=None, client_ip='pkg', print_n=1, zpl_content=None):
411
463
  """
412
464
  The main print method. Accepts info to determine the desired
413
- printer IP and to request the desired ZPL string to be sent
414
- to the printer.
415
-
416
- lab = top level key in self.printers['labs']
417
- printer_name = key for printer info (ie: ip_address) needed
418
- to satisfy print requests.
419
- label_zpl_style = template code, see above for addl deets
420
- client_ip = optional, this is logged with print request info
421
- print_n = integer, > 0
422
- zpl_content = DO NOT USE -- hacky way to directly pass a zpl
423
- string to a printer. to do: write a cleaner
424
- string+ip method of printing.
425
- """
465
+ printer IP and to request the desired ZPL string to be sent
466
+ to the printer.
426
467
 
468
+ Args:
469
+ lab: top level key in self.printers['labs']
470
+ printer_name: key for printer info (ie: ip_address) needed
471
+ to satisfy print requests.
472
+ label_zpl_style: template code, see above for addl deets
473
+ client_ip: optional, this is logged with print request info
474
+ print_n: integer, > 0
475
+ zpl_content: DO NOT USE -- hacky way to directly pass a zpl
476
+ string to a printer. to do: write a cleaner
477
+ string+ip method of printing.
478
+ """
427
479
  if print_n < 1:
428
480
  raise Exception(f"\n\nprint_n < 1 , specified {print_n}")
429
481
 
430
- rec_date = str(datetime.datetime.now()).replace(' ','_')
482
+ rec_date = str(datetime.datetime.now()).replace(' ', '_')
431
483
  print_n = int(print_n)
432
484
 
433
- if printer_name in ['','None',None] and lab in [None,'','None']:
485
+ if printer_name in ['', 'None', None] and lab in [None, '', 'None']:
434
486
  raise Exception(f"lab and printer_name are both required to route a zebra print request, the following was what was received: lab:{lab} & printer_name:{printer_name}")
435
-
436
- if label_zpl_style in [None,'','None']:
437
- label_zpl_style = self.printers['labs'][lab][printer_name]['label_zpl_styles'][0] # If a style is not specified, assume the first
438
- elif label_zpl_style not in self.printers['labs'][lab][printer_name]['label_zpl_styles']:
487
+
488
+ # Access printer via nested 'printers' key (v2 schema)
489
+ printer_data = self.printers['labs'][lab]['printers'][printer_name]
490
+
491
+ if label_zpl_style in [None, '', 'None']:
492
+ # Use default_label_style if set, otherwise fall back to first in list
493
+ label_zpl_style = printer_data.get('default_label_style') or printer_data['label_zpl_styles'][0]
494
+ elif label_zpl_style not in printer_data['label_zpl_styles']:
439
495
  _log.warning(
440
496
  "ZPL style '%s' is not valid for %s/%s. Valid styles: %s",
441
497
  label_zpl_style, lab, printer_name,
442
- self.printers['labs'][lab][printer_name]['label_zpl_styles']
498
+ printer_data['label_zpl_styles']
443
499
  )
444
500
 
445
- printer_ip = self.printers['labs'][lab][printer_name]["ip_address"]
501
+ printer_ip = printer_data["ip_address"]
446
502
 
447
503
  zpl_string = ''
448
504
  if zpl_content in [None]:
@@ -0,0 +1,59 @@
1
+ {% extends "modern/base.html" %}
2
+
3
+ {% block title %}Config Backups - Zebra Day{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <h1 class="page-title">Configuration Backups</h1>
8
+ <div class="header-actions">
9
+ <a href="/config" class="btn btn-outline"><i class="fas fa-arrow-left"></i> Back</a>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="card">
14
+ <div class="card-header">
15
+ <h3 class="card-title"><i class="fas fa-history"></i> Saved Configuration Files</h3>
16
+ <span class="badge">{{ backup_files | length }} backups</span>
17
+ </div>
18
+
19
+ <p class="text-muted mb-md" style="padding: 0 1rem;">
20
+ To restore a backup, download the file, open in a text editor, copy the contents,
21
+ then paste into the <a href="/config/edit">config editor</a> and save.
22
+ </p>
23
+
24
+ {% if backup_files %}
25
+ <div class="table-container">
26
+ <table class="table">
27
+ <thead>
28
+ <tr>
29
+ <th>Filename</th>
30
+ <th>Actions</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ {% for file in backup_files %}
35
+ <tr>
36
+ <td>
37
+ <i class="fas fa-file-code text-muted"></i>
38
+ {{ file }}
39
+ </td>
40
+ <td>
41
+ <a href="/etc/old_printer_config/{{ file }}" class="btn btn-sm btn-outline" download>
42
+ <i class="fas fa-download"></i> Download
43
+ </a>
44
+ </td>
45
+ </tr>
46
+ {% endfor %}
47
+ </tbody>
48
+ </table>
49
+ </div>
50
+ {% else %}
51
+ <div class="empty-state">
52
+ <i class="fas fa-history"></i>
53
+ <p>No backup files found</p>
54
+ <small class="text-muted">Backups are created automatically when you save configuration changes</small>
55
+ </div>
56
+ {% endif %}
57
+ </div>
58
+ {% endblock %}
59
+
@@ -0,0 +1,95 @@
1
+ {% extends "modern/base.html" %}
2
+
3
+ {% block title %}{{ title }} - Zebra Day{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <h1 class="page-title">{{ title }}</h1>
8
+ <div class="header-actions">
9
+ <a href="/config" class="btn btn-outline"><i class="fas fa-arrow-left"></i> Back</a>
10
+ {% if mode == 'edit' %}
11
+ <button type="button" class="btn btn-outline" onclick="resetConfig()"><i class="fas fa-undo"></i> Reset</button>
12
+ <button type="button" class="btn btn-outline" onclick="clearConfig()"><i class="fas fa-trash"></i> Clear</button>
13
+ {% endif %}
14
+ </div>
15
+ </div>
16
+
17
+ {% if error_msg %}
18
+ <div class="alert alert-error mb-lg">
19
+ <i class="fas fa-exclamation-circle"></i> {{ error_msg }}
20
+ </div>
21
+ {% endif %}
22
+
23
+ <div class="card">
24
+ <div class="card-header">
25
+ <h3 class="card-title"><i class="fas fa-file-code"></i> Printer Configuration JSON</h3>
26
+ {% if mode == 'edit' %}
27
+ <span class="badge badge-warning">Editing</span>
28
+ {% else %}
29
+ <span class="badge badge-info">Read Only</span>
30
+ {% endif %}
31
+ </div>
32
+ <form method="post" action="/config/save">
33
+ <div class="form-group">
34
+ <textarea name="json_data" id="json-editor" class="form-control code-editor" rows="30" {% if mode != 'edit' %}readonly{% endif %}>{{ config_json }}</textarea>
35
+ </div>
36
+ {% if mode == 'edit' %}
37
+ <div class="form-actions">
38
+ <button type="submit" class="btn btn-primary" onclick="showLoading('Saving configuration...')">
39
+ <i class="fas fa-save"></i> Save Configuration
40
+ </button>
41
+ </div>
42
+ {% endif %}
43
+ </form>
44
+ </div>
45
+
46
+ <style>
47
+ .code-editor {
48
+ font-family: 'JetBrains Mono', 'Consolas', monospace;
49
+ font-size: 0.875rem;
50
+ line-height: 1.5;
51
+ background: var(--color-gray-700);
52
+ border: 1px solid var(--color-gray-600);
53
+ padding: 1rem;
54
+ resize: vertical;
55
+ min-height: 400px;
56
+ }
57
+ .code-editor:read-only {
58
+ background: var(--color-gray-800);
59
+ cursor: default;
60
+ }
61
+ .form-actions {
62
+ display: flex;
63
+ gap: 1rem;
64
+ padding: 1rem;
65
+ border-top: 1px solid var(--color-gray-600);
66
+ }
67
+ </style>
68
+
69
+ <script>
70
+ async function resetConfig() {
71
+ if (confirm('Reset configuration to template defaults? This will backup the current config.')) {
72
+ showLoading('Resetting configuration...');
73
+ window.location.href = '/config/reset';
74
+ }
75
+ }
76
+
77
+ async function clearConfig() {
78
+ if (confirm('Clear all printer configuration? This will backup the current config.')) {
79
+ showLoading('Clearing configuration...');
80
+ window.location.href = '/config/clear';
81
+ }
82
+ }
83
+
84
+ // Auto-format JSON on blur
85
+ document.getElementById('json-editor')?.addEventListener('blur', function() {
86
+ try {
87
+ const parsed = JSON.parse(this.value);
88
+ this.value = JSON.stringify(parsed, null, 4);
89
+ } catch (e) {
90
+ // Invalid JSON, leave as-is
91
+ }
92
+ });
93
+ </script>
94
+ {% endblock %}
95
+
@@ -0,0 +1,93 @@
1
+ {% extends "modern/base.html" %}
2
+
3
+ {% block title %}New Configuration - Zebra Day{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="page-header">
7
+ <h1 class="page-title">Build New Configuration</h1>
8
+ <div class="header-actions">
9
+ <a href="/config" class="btn btn-outline"><i class="fas fa-arrow-left"></i> Back</a>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="grid grid-2">
14
+ <!-- Network Scan -->
15
+ <div class="card">
16
+ <div class="card-header">
17
+ <h3 class="card-title"><i class="fas fa-wifi"></i> Scan Network for Printers</h3>
18
+ </div>
19
+ <form action="/config/scan" method="get">
20
+ <div class="form-group">
21
+ <label class="form-label">IP Range Prefix</label>
22
+ <input type="text" name="ip_stub" class="form-control" value="{{ ip_root }}" placeholder="192.168.1">
23
+ <small class="text-muted">Enter the first three octets of your network</small>
24
+ </div>
25
+ <div class="form-group">
26
+ <label class="form-label">Lab Name</label>
27
+ <input type="text" name="lab" class="form-control" value="scan-results" placeholder="Enter lab name">
28
+ <small class="text-muted">Printers found will be added to this lab</small>
29
+ </div>
30
+ <div class="form-group">
31
+ <label class="form-label">Scan Wait Time</label>
32
+ <select name="scan_wait" class="form-control">
33
+ <option value="0.15">Fast (0.15s)</option>
34
+ <option value="0.25" selected>Normal (0.25s)</option>
35
+ <option value="0.5">Slow (0.5s)</option>
36
+ <option value="1.0">Very Slow (1.0s)</option>
37
+ </select>
38
+ <small class="text-muted">Longer wait times find more printers on slow networks</small>
39
+ </div>
40
+ <button type="submit" class="btn btn-primary" onclick="showLoading('Scanning network for Zebra printers...')">
41
+ <i class="fas fa-search"></i> Start Network Scan
42
+ </button>
43
+ </form>
44
+ </div>
45
+
46
+ <!-- Quick Actions -->
47
+ <div class="card">
48
+ <div class="card-header">
49
+ <h3 class="card-title"><i class="fas fa-bolt"></i> Quick Actions</h3>
50
+ </div>
51
+ <div class="quick-actions">
52
+ <a href="/config/reset" class="action-card" onclick="return confirm('Reset config to template defaults?')">
53
+ <i class="fas fa-undo"></i>
54
+ <div>
55
+ <strong>Reset to Template</strong>
56
+ <small class="text-muted d-block">Restore default configuration</small>
57
+ </div>
58
+ </a>
59
+ <a href="/config/clear" class="action-card" onclick="return confirm('Clear all printer configuration?')">
60
+ <i class="fas fa-trash"></i>
61
+ <div>
62
+ <strong>Clear Configuration</strong>
63
+ <small class="text-muted d-block">Remove all printers</small>
64
+ </div>
65
+ </a>
66
+ <a href="/config/edit" class="action-card">
67
+ <i class="fas fa-edit"></i>
68
+ <div>
69
+ <strong>Manual Edit</strong>
70
+ <small class="text-muted d-block">Edit JSON directly</small>
71
+ </div>
72
+ </a>
73
+ </div>
74
+
75
+ <hr class="my-md">
76
+
77
+ <h4 class="mb-md">Existing Labs</h4>
78
+ {% if labs %}
79
+ <div class="grid grid-3">
80
+ {% for lab in labs %}
81
+ <a href="/printers/{{ lab }}" class="action-card">
82
+ <i class="fas fa-building"></i>
83
+ <strong>{{ lab }}</strong>
84
+ </a>
85
+ {% endfor %}
86
+ </div>
87
+ {% else %}
88
+ <p class="text-muted">No labs configured yet. Run a network scan to discover printers.</p>
89
+ {% endif %}
90
+ </div>
91
+ </div>
92
+ {% endblock %}
93
+
@@ -116,12 +116,16 @@
116
116
 
117
117
  printerSelect.innerHTML = '<option value="">Select a printer...</option>';
118
118
 
119
- if (lab && labsDict[lab]) {
120
- for (var printer in labsDict[lab]) {
119
+ if (lab && labsDict[lab] && labsDict[lab].printers) {
120
+ // v2 schema: printers are nested under 'printers' key
121
+ var printers = labsDict[lab].printers;
122
+ for (var printerId in printers) {
123
+ var printerInfo = printers[printerId];
121
124
  var option = document.createElement('option');
122
- option.value = printer;
123
- option.text = printer;
124
- if (printer === selectedPrinter) {
125
+ option.value = printerId;
126
+ // Display friendly name if available, otherwise use ID
127
+ option.text = printerInfo.printer_name || printerId;
128
+ if (printerId === selectedPrinter) {
125
129
  option.selected = true;
126
130
  }
127
131
  printerSelect.appendChild(option);