zebra-day 0.0.37__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 (173) hide show
  1. zebra_day/__init__.py +35 -0
  2. zebra_day/bin/__init__.py +0 -0
  3. zebra_day/cli/__init__.py +240 -0
  4. zebra_day/cli/cognito.py +121 -0
  5. zebra_day/cli/gui.py +338 -0
  6. zebra_day/cli/printer.py +168 -0
  7. zebra_day/cli/template.py +176 -0
  8. zebra_day/cmd_mgr.py +35 -0
  9. zebra_day/etc/Monoid-Regular-HalfTight-Dollar-0-1-l.ttf +0 -0
  10. zebra_day/etc/label_styles/blank.zpl +0 -0
  11. zebra_day/etc/label_styles/cornersStripOf4Squares_1inX1in.zpl +55 -0
  12. zebra_day/etc/label_styles/corners_1inX2in.zpl +28 -0
  13. zebra_day/etc/label_styles/corners_20cmX30cm.zpl +6 -0
  14. zebra_day/etc/label_styles/corners_smallTube.zpl +7 -0
  15. zebra_day/etc/label_styles/corners_unspecifiedDimensions.zpl +15 -0
  16. zebra_day/etc/label_styles/generic_2inX1in.zpl +21 -0
  17. zebra_day/etc/label_styles/plate_1inX0.25in.zpl +9 -0
  18. zebra_day/etc/label_styles/plate_1inX0.25inHD.zpl +9 -0
  19. zebra_day/etc/label_styles/smallTubeWdotHD_prod.zpl +8 -0
  20. zebra_day/etc/label_styles/smallTubeWdot_corners.zpl +7 -0
  21. zebra_day/etc/label_styles/smallTubeWdot_prod.zpl +8 -0
  22. zebra_day/etc/label_styles/smallTubeWdot_prodAlt1.zpl +6 -0
  23. zebra_day/etc/label_styles/smallTubeWdot_prodAlt1b.zpl +3 -0
  24. zebra_day/etc/label_styles/smallTubeWdot_prodV2.zpl +8 -0
  25. zebra_day/etc/label_styles/smallTubeWdot_reagent.zpl +29 -0
  26. zebra_day/etc/label_styles/stripOf4Squares_1inX1in.zpl +32 -0
  27. zebra_day/etc/label_styles/test_800dX800dCoordinateArray.zpl +1 -0
  28. zebra_day/etc/label_styles/tmps/.hold +0 -0
  29. zebra_day/etc/label_styles/tmps/tmp_zpl_templates.here +0 -0
  30. zebra_day/etc/label_styles/tube_20mmX30mmA.zpl +7 -0
  31. zebra_day/etc/label_styles/tube_2inX0.3in.zpl +15 -0
  32. zebra_day/etc/label_styles/tube_2inX0.5in.zpl +15 -0
  33. zebra_day/etc/label_styles/tube_2inX0.5inHD.zpl +15 -0
  34. zebra_day/etc/label_styles/tube_2inX1in.zpl +25 -0
  35. zebra_day/etc/label_styles/tube_2inX1inHD.zpl +22 -0
  36. zebra_day/etc/label_styles/tube_2inX1inHDv3.zpl +21 -0
  37. zebra_day/etc/old_printer_config/.hold +0 -0
  38. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.022846_printer_config.json +1 -0
  39. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.033657_printer_config.json +1 -0
  40. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.039597_printer_config.json +3 -0
  41. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.047295_printer_config.json +1 -0
  42. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.055804_printer_config.json +1 -0
  43. zebra_day/etc/old_printer_config/2026-02-01_01:50:25.061337_printer_config.json +3 -0
  44. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.073326_printer_config.json +1 -0
  45. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.081950_printer_config.json +1 -0
  46. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.088251_printer_config.json +3 -0
  47. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.096501_printer_config.json +1 -0
  48. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.104767_printer_config.json +1 -0
  49. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.110364_printer_config.json +3 -0
  50. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.118239_printer_config.json +1 -0
  51. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.125950_printer_config.json +1 -0
  52. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.349866_printer_config.json +1 -0
  53. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.361085_printer_config.json +3 -0
  54. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.558323_printer_config.json +1 -0
  55. zebra_day/etc/old_printer_config/2026-02-01_01:51:24.565756_printer_config.json +3 -0
  56. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.739070_printer_config.json +16 -0
  57. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.753796_printer_config.json +1 -0
  58. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.760201_printer_config.json +3 -0
  59. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.768747_printer_config.json +1 -0
  60. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.775312_printer_config.json +3 -0
  61. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.782533_printer_config.json +1 -0
  62. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.789287_printer_config.json +1 -0
  63. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.794230_printer_config.json +3 -0
  64. zebra_day/etc/old_printer_config/2026-02-01_01:51:29.800021_printer_config.json +5 -0
  65. zebra_day/etc/printer_config.json +4 -0
  66. zebra_day/etc/printer_config.template.json +24 -0
  67. zebra_day/etc/tmp_printers0.json +5 -0
  68. zebra_day/etc/tmp_printers120.json +10 -0
  69. zebra_day/etc/tmp_printers145.json +10 -0
  70. zebra_day/etc/tmp_printers207.json +10 -0
  71. zebra_day/etc/tmp_printers374.json +5 -0
  72. zebra_day/etc/tmp_printers383.json +5 -0
  73. zebra_day/etc/tmp_printers450.json +5 -0
  74. zebra_day/etc/tmp_printers469.json +10 -0
  75. zebra_day/etc/tmp_printers485.json +10 -0
  76. zebra_day/etc/tmp_printers504.json +5 -0
  77. zebra_day/etc/tmp_printers531.json +10 -0
  78. zebra_day/etc/tmp_printers540.json +10 -0
  79. zebra_day/etc/tmp_printers542.json +10 -0
  80. zebra_day/etc/tmp_printers552.json +10 -0
  81. zebra_day/etc/tmp_printers608.json +5 -0
  82. zebra_day/etc/tmp_printers657.json +5 -0
  83. zebra_day/etc/tmp_printers715.json +10 -0
  84. zebra_day/etc/tmp_printers838.json +5 -0
  85. zebra_day/etc/tmp_printers839.json +5 -0
  86. zebra_day/etc/tmp_printers933.json +5 -0
  87. zebra_day/etc/tmp_printers957.json +5 -0
  88. zebra_day/etc/tmp_printers972.json +10 -0
  89. zebra_day/exceptions.py +88 -0
  90. zebra_day/files/.hold +0 -0
  91. zebra_day/files/blank_preview.png +0 -0
  92. zebra_day/files/corners_20cmX30cm_preview.png +0 -0
  93. zebra_day/files/generic_2inX1in_preview.png +0 -0
  94. zebra_day/files/hold +0 -0
  95. zebra_day/files/test_png_12020.png +0 -0
  96. zebra_day/files/test_png_12352.png +0 -0
  97. zebra_day/files/test_png_15472.png +0 -0
  98. zebra_day/files/test_png_17696.png +0 -0
  99. zebra_day/files/test_png_23477.png +0 -0
  100. zebra_day/files/test_png_24493.png +0 -0
  101. zebra_day/files/test_png_28157.png +0 -0
  102. zebra_day/files/test_png_30069.png +0 -0
  103. zebra_day/files/test_png_35832.png +0 -0
  104. zebra_day/files/test_png_36400.png +0 -0
  105. zebra_day/files/test_png_40816.png +0 -0
  106. zebra_day/files/test_png_47791.png +0 -0
  107. zebra_day/files/test_png_47799.png +0 -0
  108. zebra_day/files/test_png_49564.png +0 -0
  109. zebra_day/files/test_png_53848.png +0 -0
  110. zebra_day/files/test_png_55588.png +0 -0
  111. zebra_day/files/test_png_58809.png +0 -0
  112. zebra_day/files/test_png_62542.png +0 -0
  113. zebra_day/files/test_png_67242.png +0 -0
  114. zebra_day/files/test_png_89893.png +0 -0
  115. zebra_day/files/test_png_91597.png +0 -0
  116. zebra_day/files/test_png_93633.png +0 -0
  117. zebra_day/files/tmpbjo3k7q1.png +0 -0
  118. zebra_day/files/tmpigtr4pwy.png +0 -0
  119. zebra_day/files/tube_20mmX30mmA_preview.png +0 -0
  120. zebra_day/files/zpl_label_tube_2inX1in_2026-02-01_01:51:24.370964.png +0 -0
  121. zebra_day/logging_config.py +74 -0
  122. zebra_day/logs/.hold +0 -0
  123. zebra_day/logs/print_requests.log +2 -0
  124. zebra_day/paths.py +143 -0
  125. zebra_day/print_mgr.py +557 -117
  126. zebra_day/static/datschund.css +140 -0
  127. zebra_day/static/datschund.png +0 -0
  128. zebra_day/static/daylily.png +0 -0
  129. zebra_day/static/favicon.svg +20 -0
  130. zebra_day/static/general.css +99 -0
  131. zebra_day/static/js/zebra_modern.js +172 -0
  132. zebra_day/static/lsmc.css +354 -0
  133. zebra_day/static/moon.jpeg +0 -0
  134. zebra_day/static/oakland.css +197 -0
  135. zebra_day/static/petrichor.css +150 -0
  136. zebra_day/static/popday_daylily.css +140 -0
  137. zebra_day/static/style.css +183 -0
  138. zebra_day/static/triangles.css +122 -0
  139. zebra_day/static/tron.css +277 -0
  140. zebra_day/static/zebra_modern.css +771 -0
  141. zebra_day/static/zebras.css +176 -0
  142. zebra_day/templates/modern/base.html +98 -0
  143. zebra_day/templates/modern/config.html +141 -0
  144. zebra_day/templates/modern/config_backups.html +59 -0
  145. zebra_day/templates/modern/config_editor.html +95 -0
  146. zebra_day/templates/modern/config_new.html +93 -0
  147. zebra_day/templates/modern/dashboard.html +160 -0
  148. zebra_day/templates/modern/print_request.html +145 -0
  149. zebra_day/templates/modern/print_result.html +88 -0
  150. zebra_day/templates/modern/printer_detail.html +244 -0
  151. zebra_day/templates/modern/printers.html +144 -0
  152. zebra_day/templates/modern/save_result.html +46 -0
  153. zebra_day/templates/modern/template_editor.html +175 -0
  154. zebra_day/templates/modern/templates.html +122 -0
  155. zebra_day/web/__init__.py +9 -0
  156. zebra_day/web/app.py +248 -0
  157. zebra_day/web/auth.py +172 -0
  158. zebra_day/web/middleware.py +159 -0
  159. zebra_day/web/routers/__init__.py +2 -0
  160. zebra_day/web/routers/api.py +313 -0
  161. zebra_day/web/routers/ui.py +636 -0
  162. zebra_day/zpl_renderer.py +273 -0
  163. zebra_day-2.0.0.dist-info/METADATA +847 -0
  164. zebra_day-2.0.0.dist-info/RECORD +168 -0
  165. {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info}/WHEEL +1 -1
  166. zebra_day-2.0.0.dist-info/entry_points.txt +4 -0
  167. zebra_day/bin/scan_for_networed_zebra_printers.py +0 -23
  168. zebra_day/bin/te.py +0 -905
  169. zebra_day/bin/zserve.py +0 -620
  170. zebra_day-0.0.37.dist-info/METADATA +0 -1177
  171. zebra_day-0.0.37.dist-info/RECORD +0 -10
  172. {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info/licenses}/LICENSE +0 -0
  173. {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info}/top_level.txt +0 -0
zebra_day/cli/gui.py ADDED
@@ -0,0 +1,338 @@
1
+ """GUI server management commands for zebra_day CLI."""
2
+
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional, Literal
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from zebra_day import paths as xdg
16
+
17
+ gui_app = typer.Typer(help="Web UI server management commands")
18
+ console = Console()
19
+
20
+ # PID and log file locations
21
+ STATE_DIR = xdg.get_state_dir()
22
+ LOG_DIR = xdg.get_logs_dir()
23
+ CONFIG_DIR = xdg.get_config_dir()
24
+ PID_FILE = STATE_DIR / "gui.pid"
25
+ DEFAULT_CERT_DIR = CONFIG_DIR / "certs"
26
+ DEFAULT_CERT_FILE = DEFAULT_CERT_DIR / "server.crt"
27
+ DEFAULT_KEY_FILE = DEFAULT_CERT_DIR / "server.key"
28
+
29
+
30
+ def _ensure_dirs():
31
+ """Ensure state and log directories exist."""
32
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
33
+ LOG_DIR.mkdir(parents=True, exist_ok=True)
34
+
35
+
36
+ def _get_log_file() -> Path:
37
+ """Get timestamped log file path."""
38
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
39
+ return LOG_DIR / f"gui_{ts}.log"
40
+
41
+
42
+ def _get_latest_log() -> Optional[Path]:
43
+ """Get the most recent log file."""
44
+ logs = sorted(LOG_DIR.glob("gui_*.log"), reverse=True)
45
+ return logs[0] if logs else None
46
+
47
+
48
+ def _get_pid() -> Optional[int]:
49
+ """Get the running server PID if exists."""
50
+ if PID_FILE.exists():
51
+ try:
52
+ pid = int(PID_FILE.read_text().strip())
53
+ os.kill(pid, 0)
54
+ return pid
55
+ except (ValueError, ProcessLookupError, PermissionError):
56
+ PID_FILE.unlink(missing_ok=True)
57
+ return None
58
+
59
+
60
+ def _check_auth_dependencies() -> bool:
61
+ """Check if auth dependencies are available."""
62
+ try:
63
+ import jose # noqa: F401
64
+ return True
65
+ except ImportError:
66
+ return False
67
+
68
+
69
+ def _resolve_ssl_paths(
70
+ cert: Optional[str], key: Optional[str]
71
+ ) -> tuple[Optional[str], Optional[str], bool]:
72
+ """
73
+ Resolve SSL certificate and key paths.
74
+
75
+ Priority:
76
+ 1. Explicit --cert/--key arguments
77
+ 2. SSL_CERT_PATH/SSL_KEY_PATH environment variables
78
+ 3. Default paths in ~/.config/zebra_day/certs/
79
+
80
+ Returns:
81
+ Tuple of (cert_path, key_path, use_https)
82
+ """
83
+ cert_path = cert
84
+ key_path = key
85
+
86
+ # Check environment variables
87
+ if not cert_path:
88
+ cert_path = os.environ.get("SSL_CERT_PATH")
89
+ if not key_path:
90
+ key_path = os.environ.get("SSL_KEY_PATH")
91
+
92
+ # Check default paths
93
+ if not cert_path and DEFAULT_CERT_FILE.exists():
94
+ cert_path = str(DEFAULT_CERT_FILE)
95
+ if not key_path and DEFAULT_KEY_FILE.exists():
96
+ key_path = str(DEFAULT_KEY_FILE)
97
+
98
+ # Validate both exist
99
+ if cert_path and key_path:
100
+ if Path(cert_path).exists() and Path(key_path).exists():
101
+ return cert_path, key_path, True
102
+
103
+ return None, None, False
104
+
105
+
106
+ @gui_app.command("start")
107
+ def start(
108
+ port: int = typer.Option(8118, "--port", "-p", help="Port to run the server on"),
109
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
110
+ auth: str = typer.Option("none", "--auth", "-a", help="Authentication mode: none or cognito"),
111
+ reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload (foreground)"),
112
+ background: bool = typer.Option(True, "--background/--foreground", "-b/-f", help="Run in background"),
113
+ cert: Optional[str] = typer.Option(None, "--cert", help="Path to SSL certificate file"),
114
+ key: Optional[str] = typer.Option(None, "--key", help="Path to SSL private key file"),
115
+ no_https: bool = typer.Option(False, "--no-https", help="Disable HTTPS even if certificates are available"),
116
+ ):
117
+ """Start the zebra_day web UI server.
118
+
119
+ By default, HTTPS is enabled if certificates are found in:
120
+ - Explicit --cert/--key arguments
121
+ - SSL_CERT_PATH/SSL_KEY_PATH environment variables
122
+ - ~/.config/zebra_day/certs/server.crt and server.key
123
+
124
+ Use --no-https to force HTTP mode.
125
+ """
126
+ _ensure_dirs()
127
+
128
+ # Validate auth option
129
+ if auth not in ("none", "cognito"):
130
+ console.print(f"[red]✗[/red] Invalid auth mode: {auth}. Use 'none' or 'cognito'.")
131
+ raise typer.Exit(1)
132
+
133
+ # Check if already running
134
+ pid = _get_pid()
135
+ if pid:
136
+ console.print(f"[yellow]⚠[/yellow] Server already running (PID {pid})")
137
+ console.print(f" URL: [cyan]http://{host}:{port}[/cyan]")
138
+ return
139
+
140
+ # Check auth dependencies if cognito mode
141
+ if auth == "cognito":
142
+ if not _check_auth_dependencies():
143
+ console.print("[red]✗[/red] Authentication requested but python-jose is not installed")
144
+ console.print(" Install with: [cyan]pip install -e \".[auth]\"[/cyan]")
145
+ raise typer.Exit(1)
146
+
147
+ # Check required env vars
148
+ missing = []
149
+ if not os.environ.get("COGNITO_USER_POOL_ID"):
150
+ missing.append("COGNITO_USER_POOL_ID")
151
+ if not os.environ.get("COGNITO_APP_CLIENT_ID"):
152
+ missing.append("COGNITO_APP_CLIENT_ID")
153
+ if missing:
154
+ console.print("[red]✗[/red] Cognito auth enabled but environment variables missing:")
155
+ for var in missing:
156
+ console.print(f" • {var}")
157
+ raise typer.Exit(1)
158
+ console.print("[green]✓[/green] Cognito authentication enabled")
159
+
160
+ # Resolve SSL paths
161
+ cert_path, key_path, use_https = _resolve_ssl_paths(cert, key)
162
+
163
+ if no_https:
164
+ use_https = False
165
+ cert_path = None
166
+ key_path = None
167
+
168
+ protocol = "https" if use_https else "http"
169
+
170
+ if use_https:
171
+ console.print(f"[green]✓[/green] HTTPS enabled")
172
+ console.print(f" Certificate: [dim]{cert_path}[/dim]")
173
+ console.print(f" Private key: [dim]{key_path}[/dim]")
174
+ else:
175
+ console.print("[yellow]⚠[/yellow] Running in HTTP mode (insecure)")
176
+ console.print(" For HTTPS, generate certificates with mkcert:")
177
+ console.print(f" [dim]mkdir -p {DEFAULT_CERT_DIR}[/dim]")
178
+ console.print(f" [dim]mkcert -cert-file {DEFAULT_CERT_FILE} -key-file {DEFAULT_KEY_FILE} localhost 127.0.0.1 ::1[/dim]")
179
+
180
+ # Build command with SSL parameters
181
+ ssl_args = ""
182
+ if use_https and cert_path and key_path:
183
+ ssl_args = f", ssl_certfile='{cert_path}', ssl_keyfile='{key_path}'"
184
+
185
+ cmd = [
186
+ sys.executable,
187
+ "-c",
188
+ f"from zebra_day.web.app import run_server; run_server(host='{host}', port={port}, reload={reload}, auth='{auth}'{ssl_args})",
189
+ ]
190
+
191
+ # Set up environment
192
+ env = os.environ.copy()
193
+ env["PYTHONUNBUFFERED"] = "1"
194
+ env["ZEBRA_DAY_AUTH_MODE"] = auth
195
+ if cert_path:
196
+ env["SSL_CERT_PATH"] = cert_path
197
+ if key_path:
198
+ env["SSL_KEY_PATH"] = key_path
199
+
200
+ if reload:
201
+ background = False
202
+ console.print("[dim]Auto-reload enabled (foreground mode)[/dim]")
203
+
204
+ if background:
205
+ log_file = _get_log_file()
206
+ log_f = open(log_file, "w", buffering=1)
207
+
208
+ proc = subprocess.Popen(
209
+ cmd,
210
+ stdout=log_f,
211
+ stderr=subprocess.STDOUT,
212
+ start_new_session=True,
213
+ cwd=Path.cwd(),
214
+ env=env,
215
+ )
216
+
217
+ time.sleep(2)
218
+ if proc.poll() is not None:
219
+ log_f.close()
220
+ console.print("[red]✗[/red] Server failed to start. Check logs:")
221
+ console.print(f" [dim]{log_file}[/dim]")
222
+ if log_file.exists():
223
+ content = log_file.read_text().strip()
224
+ if content:
225
+ console.print("\n[dim]--- Last error ---[/dim]")
226
+ for line in content.split("\n")[-10:]:
227
+ console.print(f" {line}")
228
+ raise typer.Exit(1)
229
+
230
+ PID_FILE.write_text(str(proc.pid))
231
+ console.print(f"[green]✓[/green] Server started (PID {proc.pid})")
232
+ console.print(f" URL: [cyan]{protocol}://{host}:{port}[/cyan]")
233
+ console.print(f" Logs: [dim]{log_file}[/dim]")
234
+ else:
235
+ console.print(f"[green]✓[/green] Starting server on [cyan]{protocol}://{host}:{port}[/cyan]")
236
+ console.print(" Press Ctrl+C to stop\n")
237
+ try:
238
+ result = subprocess.run(cmd, cwd=Path.cwd(), env=env)
239
+ if result.returncode != 0:
240
+ raise typer.Exit(result.returncode)
241
+ except KeyboardInterrupt:
242
+ console.print("\n[yellow]⚠[/yellow] Server stopped")
243
+
244
+
245
+ @gui_app.command("stop")
246
+ def stop():
247
+ """Stop the zebra_day web UI server."""
248
+ pid = _get_pid()
249
+ if not pid:
250
+ console.print("[yellow]⚠[/yellow] No server running")
251
+ return
252
+
253
+ try:
254
+ os.kill(pid, signal.SIGTERM)
255
+ for _ in range(10):
256
+ time.sleep(0.5)
257
+ try:
258
+ os.kill(pid, 0)
259
+ except ProcessLookupError:
260
+ break
261
+ else:
262
+ os.kill(pid, signal.SIGKILL)
263
+
264
+ PID_FILE.unlink(missing_ok=True)
265
+ console.print(f"[green]✓[/green] Server stopped (was PID {pid})")
266
+ except ProcessLookupError:
267
+ PID_FILE.unlink(missing_ok=True)
268
+ console.print("[yellow]⚠[/yellow] Server was not running")
269
+ except PermissionError:
270
+ console.print(f"[red]✗[/red] Permission denied stopping PID {pid}")
271
+ raise typer.Exit(1)
272
+
273
+
274
+ @gui_app.command("status")
275
+ def status():
276
+ """Check the status of the zebra_day web UI server."""
277
+ pid = _get_pid()
278
+ if pid:
279
+ log_file = _get_latest_log()
280
+ # Check if HTTPS is likely enabled based on cert availability
281
+ _, _, use_https = _resolve_ssl_paths(None, None)
282
+ protocol = "https" if use_https else "http"
283
+ console.print(f"[green]●[/green] Server is [green]running[/green] (PID {pid})")
284
+ console.print(f" URL: [cyan]{protocol}://0.0.0.0:8118[/cyan]")
285
+ if log_file:
286
+ console.print(f" Logs: [dim]{log_file}[/dim]")
287
+ else:
288
+ console.print("[dim]○[/dim] Server is [dim]not running[/dim]")
289
+
290
+
291
+ @gui_app.command("logs")
292
+ def logs(
293
+ lines: int = typer.Option(50, "--tail", "-n", help="Number of lines to show"),
294
+ follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
295
+ all_logs: bool = typer.Option(False, "--all", "-a", help="List all log files"),
296
+ ):
297
+ """View zebra_day web UI server logs."""
298
+ if all_logs:
299
+ log_files = sorted(LOG_DIR.glob("gui_*.log"), reverse=True)
300
+ if not log_files:
301
+ console.print("[yellow]⚠[/yellow] No log files found.")
302
+ return
303
+ console.print(f"[bold]Server log files ({len(log_files)}):[/bold]")
304
+ for lf in log_files[:20]:
305
+ size = lf.stat().st_size
306
+ console.print(f" {lf.name} [dim]({size:,} bytes)[/dim]")
307
+ return
308
+
309
+ log_file = _get_latest_log()
310
+ if not log_file:
311
+ console.print("[yellow]⚠[/yellow] No log file found. Start the server first.")
312
+ return
313
+
314
+ if follow:
315
+ console.print(f"[dim]Following {log_file.name} (Ctrl+C to stop)[/dim]\n")
316
+ try:
317
+ subprocess.run(["tail", "-f", "-n", str(lines), str(log_file)])
318
+ except KeyboardInterrupt:
319
+ console.print("\n")
320
+ else:
321
+ console.print(f"[dim]Showing last {lines} lines of {log_file.name}[/dim]\n")
322
+ try:
323
+ subprocess.run(["tail", "-n", str(lines), str(log_file)])
324
+ except Exception as e:
325
+ console.print(f"[red]✗[/red] Error reading log: {e}")
326
+
327
+
328
+ @gui_app.command("restart")
329
+ def restart(
330
+ port: int = typer.Option(8118, "--port", "-p", help="Port to run the server on"),
331
+ host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
332
+ auth: str = typer.Option("none", "--auth", "-a", help="Authentication mode: none or cognito"),
333
+ ):
334
+ """Restart the zebra_day web UI server."""
335
+ stop()
336
+ time.sleep(1)
337
+ start(port=port, host=host, auth=auth, reload=False, background=True)
338
+
@@ -0,0 +1,168 @@
1
+ """Printer fleet management commands for zebra_day CLI."""
2
+
3
+ import json
4
+ import socket
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from zebra_day import paths as xdg
12
+
13
+ printer_app = typer.Typer(help="Printer fleet management commands")
14
+ console = Console()
15
+
16
+
17
+ def _get_local_ip() -> str:
18
+ """Get local IP address."""
19
+ try:
20
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
21
+ s.connect(("8.8.8.8", 80))
22
+ ip = s.getsockname()[0]
23
+ s.close()
24
+ return ip
25
+ except Exception:
26
+ return "127.0.0.1"
27
+
28
+
29
+ @printer_app.command("scan")
30
+ def scan(
31
+ ip_stub: Optional[str] = typer.Option(None, "--ip-stub", "-i", help="IP stub to scan (e.g., 192.168.1)"),
32
+ wait: float = typer.Option(0.25, "--wait", "-w", help="Seconds to wait per IP probe"),
33
+ lab: str = typer.Option("scan-results", "--lab", "-l", help="Lab name to assign found printers"),
34
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
35
+ ):
36
+ """Scan network for Zebra printers."""
37
+ # Determine IP stub if not provided
38
+ if not ip_stub:
39
+ local_ip = _get_local_ip()
40
+ ip_stub = ".".join(local_ip.split(".")[:-1])
41
+
42
+ if not json_output:
43
+ console.print(f"[cyan]→[/cyan] Scanning {ip_stub}.* for Zebra printers...")
44
+ console.print("[dim] This may take a few minutes...[/dim]")
45
+
46
+ try:
47
+ import zebra_day.print_mgr as zdpm
48
+ zp = zdpm.zpl()
49
+ zp.probe_zebra_printers_add_to_printers_json(
50
+ ip_stub=ip_stub,
51
+ scan_wait=str(wait),
52
+ lab=lab,
53
+ )
54
+
55
+ found = []
56
+ if hasattr(zp, "printers") and "labs" in zp.printers and lab in zp.printers["labs"]:
57
+ for name, info in zp.printers["labs"][lab].items():
58
+ if isinstance(info, dict) and info.get("ip_address") not in ["dl_png"]:
59
+ found.append({
60
+ "name": name,
61
+ "ip": info.get("ip_address"),
62
+ "model": info.get("model", "unknown"),
63
+ "serial": info.get("serial", "unknown"),
64
+ })
65
+
66
+ if json_output:
67
+ console.print(json.dumps(found, indent=2))
68
+ else:
69
+ console.print(f"\n[green]✓[/green] Found {len(found)} printer(s)")
70
+ if found:
71
+ table = Table()
72
+ table.add_column("Name", style="cyan")
73
+ table.add_column("IP Address")
74
+ table.add_column("Model")
75
+ table.add_column("Serial")
76
+ for p in found:
77
+ table.add_row(p["name"], p["ip"], p["model"], p["serial"])
78
+ console.print(table)
79
+
80
+ except Exception as e:
81
+ if json_output:
82
+ console.print(json.dumps({"error": str(e)}))
83
+ else:
84
+ console.print(f"[red]✗[/red] Scan error: {e}")
85
+ raise typer.Exit(1)
86
+
87
+
88
+ @printer_app.command("list")
89
+ def list_printers(
90
+ lab: Optional[str] = typer.Option(None, "--lab", "-l", help="Filter by lab name"),
91
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
92
+ ):
93
+ """List configured printers."""
94
+ try:
95
+ import zebra_day.print_mgr as zdpm
96
+ zp = zdpm.zpl()
97
+
98
+ printers = []
99
+ if hasattr(zp, "printers") and "labs" in zp.printers:
100
+ for lab_name, lab_printers in zp.printers["labs"].items():
101
+ if lab and lab_name != lab:
102
+ continue
103
+ for name, info in lab_printers.items():
104
+ if isinstance(info, dict):
105
+ printers.append({
106
+ "lab": lab_name,
107
+ "name": name,
108
+ "ip": info.get("ip_address", "unknown"),
109
+ "model": info.get("model", "unknown"),
110
+ "styles": info.get("label_zpl_styles", []),
111
+ })
112
+
113
+ if json_output:
114
+ console.print(json.dumps(printers, indent=2))
115
+ return
116
+
117
+ if not printers:
118
+ console.print("[yellow]⚠[/yellow] No printers configured")
119
+ console.print(" Run [cyan]zday printer scan[/cyan] to discover printers")
120
+ return
121
+
122
+ table = Table(title="Configured Printers")
123
+ table.add_column("Lab", style="cyan")
124
+ table.add_column("Name")
125
+ table.add_column("IP Address")
126
+ table.add_column("Model")
127
+ table.add_column("Label Styles")
128
+ for p in printers:
129
+ styles = ", ".join(p["styles"][:2])
130
+ if len(p["styles"]) > 2:
131
+ styles += f" (+{len(p['styles'])-2})"
132
+ table.add_row(p["lab"], p["name"], p["ip"], p["model"], styles)
133
+ console.print(table)
134
+
135
+ except Exception as e:
136
+ if json_output:
137
+ console.print(json.dumps({"error": str(e)}))
138
+ else:
139
+ console.print(f"[red]✗[/red] Error: {e}")
140
+ raise typer.Exit(1)
141
+
142
+
143
+ @printer_app.command("test")
144
+ def test_print(
145
+ printer_name: str = typer.Argument(..., help="Printer name or IP address"),
146
+ lab: str = typer.Option("scan-results", "--lab", "-l", help="Lab containing the printer"),
147
+ label_style: str = typer.Option("tube_2inX1in", "--style", "-s", help="Label style to print"),
148
+ ):
149
+ """Send a test print to a specific printer."""
150
+ try:
151
+ import zebra_day.print_mgr as zdpm
152
+ zp = zdpm.zpl()
153
+
154
+ console.print(f"[cyan]→[/cyan] Sending test print to {printer_name}...")
155
+ result = zp.print_zpl(
156
+ lab=lab,
157
+ printer_name=printer_name,
158
+ uid_barcode="TEST-PRINT",
159
+ alt_a="Test Label",
160
+ alt_b="zebra_day CLI",
161
+ label_zpl_style=label_style,
162
+ )
163
+ console.print(f"[green]✓[/green] Test print sent successfully")
164
+
165
+ except Exception as e:
166
+ console.print(f"[red]✗[/red] Print error: {e}")
167
+ raise typer.Exit(1)
168
+
@@ -0,0 +1,176 @@
1
+ """ZPL template management commands for zebra_day CLI."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from zebra_day import paths as xdg
14
+
15
+ template_app = typer.Typer(help="ZPL template management commands")
16
+ console = Console()
17
+
18
+
19
+ def _get_template_dirs() -> list[Path]:
20
+ """Get all template directories."""
21
+ from importlib.resources import files
22
+
23
+ dirs = []
24
+ # XDG data directory
25
+ xdg_styles = xdg.get_label_styles_dir()
26
+ if xdg_styles.exists():
27
+ dirs.append(xdg_styles)
28
+
29
+ # Package directory
30
+ try:
31
+ pkg_styles = Path(str(files("zebra_day"))) / "etc" / "label_styles"
32
+ if pkg_styles.exists():
33
+ dirs.append(pkg_styles)
34
+ except Exception:
35
+ pass
36
+
37
+ return dirs
38
+
39
+
40
+ def _find_template(name: str) -> Optional[Path]:
41
+ """Find a template file by name."""
42
+ for template_dir in _get_template_dirs():
43
+ # Try exact match
44
+ for ext in ["", ".zpl", ".txt"]:
45
+ path = template_dir / f"{name}{ext}"
46
+ if path.exists():
47
+ return path
48
+ # Try with zpl_ prefix
49
+ for ext in ["", ".zpl", ".txt"]:
50
+ path = template_dir / f"zpl_{name}{ext}"
51
+ if path.exists():
52
+ return path
53
+ return None
54
+
55
+
56
+ @template_app.command("list")
57
+ def list_templates(
58
+ json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
59
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full paths"),
60
+ ):
61
+ """List available ZPL templates."""
62
+ templates = []
63
+
64
+ for template_dir in _get_template_dirs():
65
+ for f in template_dir.iterdir():
66
+ if f.is_file() and not f.name.startswith("."):
67
+ if f.suffix in [".zpl", ".txt", ""] or f.name.startswith("zpl_"):
68
+ templates.append({
69
+ "name": f.stem,
70
+ "path": str(f),
71
+ "size": f.stat().st_size,
72
+ "source": "user" if "zebra_day" not in str(template_dir) else "package",
73
+ })
74
+
75
+ # Dedupe by name, prefer user templates
76
+ seen = {}
77
+ for t in templates:
78
+ if t["name"] not in seen or t["source"] == "user":
79
+ seen[t["name"]] = t
80
+ templates = list(seen.values())
81
+
82
+ if json_output:
83
+ console.print(json.dumps(templates, indent=2))
84
+ return
85
+
86
+ if not templates:
87
+ console.print("[yellow]⚠[/yellow] No templates found")
88
+ return
89
+
90
+ table = Table(title="ZPL Templates")
91
+ table.add_column("Name", style="cyan")
92
+ table.add_column("Source")
93
+ table.add_column("Size")
94
+ if verbose:
95
+ table.add_column("Path", style="dim")
96
+
97
+ for t in sorted(templates, key=lambda x: x["name"]):
98
+ source_style = "[green]user[/green]" if t["source"] == "user" else "[dim]package[/dim]"
99
+ if verbose:
100
+ table.add_row(t["name"], source_style, f"{t['size']} bytes", t["path"])
101
+ else:
102
+ table.add_row(t["name"], source_style, f"{t['size']} bytes")
103
+
104
+ console.print(table)
105
+
106
+
107
+ @template_app.command("preview")
108
+ def preview(
109
+ template_name: str = typer.Argument(..., help="Template name to preview"),
110
+ output: Optional[str] = typer.Option(None, "--output", "-o", help="Output PNG file path"),
111
+ ):
112
+ """Generate a PNG preview of a ZPL template."""
113
+ template_path = _find_template(template_name)
114
+ if not template_path:
115
+ console.print(f"[red]✗[/red] Template not found: {template_name}")
116
+ raise typer.Exit(1)
117
+
118
+ console.print(f"[cyan]→[/cyan] Generating preview for {template_name}...")
119
+
120
+ try:
121
+ import zebra_day.print_mgr as zdpm
122
+ zp = zdpm.zpl()
123
+
124
+ # Read template
125
+ zpl_content = template_path.read_text()
126
+
127
+ # Generate PNG
128
+ if not output:
129
+ output_path = xdg.get_generated_files_dir() / f"{template_name}_preview.png"
130
+ else:
131
+ output_path = Path(output)
132
+
133
+ result = zp.generate_label_png(zpl_content, str(output_path), False)
134
+ console.print(f"[green]✓[/green] Preview generated: {output_path}")
135
+
136
+ except Exception as e:
137
+ console.print(f"[red]✗[/red] Preview error: {e}")
138
+ raise typer.Exit(1)
139
+
140
+
141
+ @template_app.command("edit")
142
+ def edit(
143
+ template_name: str = typer.Argument(..., help="Template name to edit"),
144
+ editor: Optional[str] = typer.Option(None, "--editor", "-e", help="Editor command"),
145
+ ):
146
+ """Open a ZPL template in an editor."""
147
+ template_path = _find_template(template_name)
148
+ if not template_path:
149
+ console.print(f"[red]✗[/red] Template not found: {template_name}")
150
+ raise typer.Exit(1)
151
+
152
+ # Determine editor
153
+ if not editor:
154
+ editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or "vi"
155
+
156
+ console.print(f"[cyan]→[/cyan] Opening {template_path} with {editor}...")
157
+ try:
158
+ subprocess.run([editor, str(template_path)])
159
+ except Exception as e:
160
+ console.print(f"[red]✗[/red] Error opening editor: {e}")
161
+ raise typer.Exit(1)
162
+
163
+
164
+ @template_app.command("show")
165
+ def show(
166
+ template_name: str = typer.Argument(..., help="Template name to display"),
167
+ ):
168
+ """Display the contents of a ZPL template."""
169
+ template_path = _find_template(template_name)
170
+ if not template_path:
171
+ console.print(f"[red]✗[/red] Template not found: {template_name}")
172
+ raise typer.Exit(1)
173
+
174
+ console.print(f"[dim]# {template_path}[/dim]\n")
175
+ console.print(template_path.read_text())
176
+