shuttlecheck 0.4.1__tar.gz → 0.4.2__tar.gz

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 (157) hide show
  1. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/PKG-INFO +1 -1
  2. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/pyproject.toml +1 -1
  3. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/cli.py +9 -2
  4. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui.py +36 -1
  5. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/app.js +12 -1
  6. shuttlecheck-0.4.2/shuttlecheck/version.py +1 -0
  7. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/PKG-INFO +1 -1
  8. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_window.py +41 -0
  9. shuttlecheck-0.4.1/shuttlecheck/version.py +0 -1
  10. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/LICENSE +0 -0
  11. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/PYPI.md +0 -0
  12. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/README.md +0 -0
  13. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/setup.cfg +0 -0
  14. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/__init__.py +0 -0
  15. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/admission.py +0 -0
  16. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ale.py +0 -0
  17. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/aliases.py +0 -0
  18. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/arrivals.py +0 -0
  19. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/attribution.py +0 -0
  20. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/auth.py +0 -0
  21. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/certificate.py +0 -0
  22. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/checks.py +0 -0
  23. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/completeness.py +0 -0
  24. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/conventions.py +0 -0
  25. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/delivery.py +0 -0
  26. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/demo.py +0 -0
  27. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/demo_convention.yaml +0 -0
  28. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/endpoints.py +0 -0
  29. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/extensions.py +0 -0
  30. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/infer.py +0 -0
  31. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/jobs.py +0 -0
  32. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/journal.py +0 -0
  33. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/layoutlog.py +0 -0
  34. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/listener.py +0 -0
  35. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/marks.py +0 -0
  36. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/mhl.py +0 -0
  37. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/originals.py +0 -0
  38. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/patterns.py +0 -0
  39. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/prefolder.py +0 -0
  40. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/probe.py +0 -0
  41. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/provenance.py +0 -0
  42. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/receive.py +0 -0
  43. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/report.py +0 -0
  44. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/say.py +0 -0
  45. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/schedule.py +0 -0
  46. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/schema.json +0 -0
  47. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/seal.py +0 -0
  48. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/seal_api.py +0 -0
  49. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/sidecar.py +0 -0
  50. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/sizing.py +0 -0
  51. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/spec.py +0 -0
  52. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/store.py +0 -0
  53. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/support.py +0 -0
  54. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/telemetry-v1.schema.json +0 -0
  55. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/telemetry.py +0 -0
  56. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/templates.py +0 -0
  57. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/today.py +0 -0
  58. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/transport.py +0 -0
  59. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/app.css +0 -0
  60. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/app.html +0 -0
  61. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/classic.css +0 -0
  62. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/classic.html +0 -0
  63. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/classic.js +0 -0
  64. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/display-LICENSE.txt +0 -0
  65. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/display.woff2 +0 -0
  66. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/inter-LICENSE.txt +0 -0
  67. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/inter.woff2 +0 -0
  68. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/mono-LICENSE.txt +0 -0
  69. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/ui_assets/fonts/mono.woff2 +0 -0
  70. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/updates.py +0 -0
  71. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/validate.py +0 -0
  72. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/walkthrough.py +0 -0
  73. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck/window.py +0 -0
  74. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/SOURCES.txt +0 -0
  75. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/dependency_links.txt +0 -0
  76. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/entry_points.txt +0 -0
  77. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/requires.txt +0 -0
  78. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/shuttlecheck.egg-info/top_level.txt +0 -0
  79. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_admission.py +0 -0
  80. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_ale.py +0 -0
  81. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_aliases.py +0 -0
  82. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_allocate.py +0 -0
  83. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_arrivals.py +0 -0
  84. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_auth.py +0 -0
  85. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_auth_packaged.py +0 -0
  86. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_check_against.py +0 -0
  87. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_classify.py +0 -0
  88. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_cli.py +0 -0
  89. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_completeness.py +0 -0
  90. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_consoles.py +0 -0
  91. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_conventions.py +0 -0
  92. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_conventions_routes.py +0 -0
  93. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_counting_files.py +0 -0
  94. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_dailies.py +0 -0
  95. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_dailiesdrop.py +0 -0
  96. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_delivery.py +0 -0
  97. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_device_flow.py +0 -0
  98. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_due.py +0 -0
  99. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_endpoints.py +0 -0
  100. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_enrolment.py +0 -0
  101. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_fleet_units.py +0 -0
  102. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_folders.py +0 -0
  103. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_fonts.py +0 -0
  104. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_groups.py +0 -0
  105. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_infer.py +0 -0
  106. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_infer_keys.py +0 -0
  107. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_jobs.py +0 -0
  108. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_keepable.py +0 -0
  109. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_lamps.py +0 -0
  110. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_listener.py +0 -0
  111. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_marks_api.py +0 -0
  112. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_message_style.py +0 -0
  113. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_meters_measure.py +0 -0
  114. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_mhl.py +0 -0
  115. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_min_version.py +0 -0
  116. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_monitor.py +0 -0
  117. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_no_convention.py +0 -0
  118. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_one_file_is_not_a_pattern.py +0 -0
  119. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_originals.py +0 -0
  120. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_path_length.py +0 -0
  121. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_pick.py +0 -0
  122. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_pointed_at_the_job.py +0 -0
  123. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_prefolder.py +0 -0
  124. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_problems.py +0 -0
  125. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_provenance.py +0 -0
  126. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_rack.py +0 -0
  127. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_receive.py +0 -0
  128. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_rename_token.py +0 -0
  129. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_report.py +0 -0
  130. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_reveal.py +0 -0
  131. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_rollcall_schedule.py +0 -0
  132. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_signal_path.py +0 -0
  133. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_sizing.py +0 -0
  134. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_sizing_history.py +0 -0
  135. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_source_attach.py +0 -0
  136. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_store.py +0 -0
  137. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_support.py +0 -0
  138. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_sweeping.py +0 -0
  139. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_telemetry.py +0 -0
  140. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_templates.py +0 -0
  141. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_the_04_window.py +0 -0
  142. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_the_analyser.py +0 -0
  143. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_the_pipeline.py +0 -0
  144. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_today.py +0 -0
  145. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_today_screen.py +0 -0
  146. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_trademark.py +0 -0
  147. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_transport.py +0 -0
  148. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_ui.py +0 -0
  149. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_ui_routes.py +0 -0
  150. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_unread_sources.py +0 -0
  151. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_updates.py +0 -0
  152. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_validate.py +0 -0
  153. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_verify.py +0 -0
  154. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_walk.py +0 -0
  155. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_web_seal.py +0 -0
  156. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_what_the_far_end_reads.py +0 -0
  157. {shuttlecheck-0.4.1 → shuttlecheck-0.4.2}/tests/test_where_it_belongs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shuttlecheck
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Check media filenames, folder structure and deliverables against a written convention. Read-only.
5
5
  Author-email: MVRK Systems <hello@mvrk.systems>
6
6
  License: Proprietary — free to use, no redistribution
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shuttlecheck"
3
- version = "0.4.1"
3
+ version = "0.4.2"
4
4
  description = "Check media filenames, folder structure and deliverables against a written convention. Read-only."
5
5
  readme = { file = "PYPI.md", content-type = "text/markdown" }
6
6
  requires-python = ">=3.11"
@@ -69,11 +69,17 @@ def build_parser() -> argparse.ArgumentParser:
69
69
  b.add_argument("--yes", action="store_true",
70
70
  help="write it without printing the contents first")
71
71
 
72
- u = sub.add_parser("ui", help="open the local app in a browser")
72
+ u = sub.add_parser("ui", help="open the local app in a browser, or in its own window")
73
73
  u.add_argument("root", nargs="?", help="folder to open on (optional)")
74
74
  u.add_argument("--port", type=int, default=8765)
75
75
  u.add_argument("--no-browser", action="store_true",
76
76
  help="print the URL instead of opening it")
77
+ # What the packaged app does, available from a checkout. Without it a
78
+ # checkout can only ever show the tool as a tab, and the window is the one
79
+ # part of the product nobody sees until a build ships.
80
+ u.add_argument("--window", action="store_true",
81
+ help="open in its own window (needs the [app] extra); "
82
+ "falls back to a browser where there is no webview")
77
83
 
78
84
  i = sub.add_parser("init", help="read a tree and write the spec it already follows")
79
85
  i.add_argument("root")
@@ -187,7 +193,8 @@ def _ui(args) -> int:
187
193
  if root and not root.is_dir():
188
194
  print(f"error: not a folder: {root}", file=sys.stderr)
189
195
  return 2
190
- serve(root, port=args.port, open_browser=not args.no_browser)
196
+ serve(root, port=args.port, open_browser=not args.no_browser,
197
+ shell=args.window and not args.no_browser)
191
198
  return 0
192
199
 
193
200
 
@@ -35,6 +35,7 @@ import secrets
35
35
  import subprocess
36
36
  import sys
37
37
  import threading
38
+ import time
38
39
  import webbrowser
39
40
  from dataclasses import asdict
40
41
  from datetime import datetime, timezone
@@ -2382,10 +2383,17 @@ ROUTES = build_routes()
2382
2383
  class Handler(BaseHTTPRequestHandler):
2383
2384
  server_version = f"ShuttleCheck/{__version__}"
2384
2385
  token = ""
2386
+ # When the page last asked for anything. Only read when the app has fallen
2387
+ # back to a browser, where it is the one sign that anybody is still there.
2388
+ last_seen = 0.0
2385
2389
 
2386
2390
  def log_message(self, *args): # keep the console for our own output
2387
2391
  pass
2388
2392
 
2393
+ def parse_request(self):
2394
+ Handler.last_seen = time.monotonic()
2395
+ return super().parse_request()
2396
+
2389
2397
  # -- helpers ----------------------------------------------------------
2390
2398
 
2391
2399
  def _send(self, status: int, body: bytes, mime: str):
@@ -2499,6 +2507,29 @@ class Handler(BaseHTTPRequestHandler):
2499
2507
  return self._json(500, {"error": f"{type(exc).__name__}: {exc}"})
2500
2508
 
2501
2509
 
2510
+ # How long a browser fallback may go unheard from before it counts as closed.
2511
+ # The page asks for /api/today every minute whatever screen is up, and a
2512
+ # background tab is throttled to about that, so ten quiet minutes is a tab that
2513
+ # was closed rather than one that is idle.
2514
+ #
2515
+ # Only the fallback needs this. A window ends the process when it closes; a tab
2516
+ # does not, and a packaged build has no console to close instead, so without
2517
+ # it the server outlives the tab and sits invisibly holding a port until the
2518
+ # next login.
2519
+ ABANDONED_AFTER = 600.0
2520
+
2521
+
2522
+ def _until_abandoned(server: threading.Thread, *,
2523
+ quiet: float = ABANDONED_AFTER, poll: float = 15.0) -> None:
2524
+ Handler.last_seen = time.monotonic()
2525
+ tick = threading.Event()
2526
+ while server.is_alive():
2527
+ tick.wait(poll)
2528
+ if time.monotonic() - Handler.last_seen > quiet:
2529
+ print(" nothing has asked for a while; stopping")
2530
+ return
2531
+
2532
+
2502
2533
  def serve(folder: Path | None = None, *, port: int = 8765,
2503
2534
  open_browser: bool = True, host: str = "127.0.0.1",
2504
2535
  shell: bool = False) -> None:
@@ -2531,7 +2562,11 @@ def serve(folder: Path | None = None, *, port: int = 8765,
2531
2562
  # and enormously better than an application that does nothing.
2532
2563
  if open_browser:
2533
2564
  window.open_browser(url)
2534
- server.join()
2565
+ _until_abandoned(server)
2566
+ else:
2567
+ # Nobody was shown anything, so there is no tab to outlive:
2568
+ # this is an ingest box, and it serves until it is stopped.
2569
+ server.join()
2535
2570
  except KeyboardInterrupt:
2536
2571
  print("\n stopped")
2537
2572
  finally:
@@ -2829,7 +2829,18 @@ PAGES.settings = async (stage) => {
2829
2829
  on("signIn", signIn);
2830
2830
  const tg = $("#telOn"); if (tg) tg.onchange = async () => { await api("/api/settings/set", { key: "telemetry.optIn", value: tg.checked }); toast(tg.checked ? "Counts will be sent." : "Nothing will be sent, and the queue was deleted."); };
2831
2831
  on("telShow", async () => { const r = await api("/api/telemetry/lastBatch"); $("#telOut").textContent = r.batch ? JSON.stringify(r.batch, null, 2) : "Nothing has been sent from this machine."; });
2832
- on("updBtn", async () => { $("#updOut").textContent = "Asking"; try { const r = await api("/api/updates/check"); $("#updOut").textContent = r.say || r.message || (r.newer ? `ShuttleCheck ${r.latest} is out.` : "This is the newest release."); } catch (err) { $("#updOut").textContent = err.message; } });
2832
+ // The feed answers with `state`. This used to read `r.newer`, which nothing
2833
+ // sends, so every answer, a release that was out and a feed that could not
2834
+ // be reached alike, came back as "This is the newest release".
2835
+ on("updBtn", async () => {
2836
+ const out = $("#updOut"); out.textContent = "Asking";
2837
+ try {
2838
+ const r = await api("/api/updates/check");
2839
+ if (r.state === "newer") out.innerHTML = `ShuttleCheck ${esc(r.latest)} is out. ${r.url ? `<a href="${esc(r.url)}" data-act="open" data-url="${esc(r.url)}">Get it</a>` : ""}`;
2840
+ else if (r.state === "current") out.textContent = `${r.running} is the newest release.`;
2841
+ else out.textContent = r.detail || "Could not ask.";
2842
+ } catch (err) { out.textContent = err.message; }
2843
+ });
2833
2844
  };
2834
2845
 
2835
2846
  async function signIn() {
@@ -0,0 +1 @@
1
+ __version__ = "0.4.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shuttlecheck
3
- Version: 0.4.1
3
+ Version: 0.4.2
4
4
  Summary: Check media filenames, folder structure and deliverables against a written convention. Read-only.
5
5
  Author-email: MVRK Systems <hello@mvrk.systems>
6
6
  License: Proprietary — free to use, no redistribution
@@ -275,6 +275,47 @@ def test_asking_for_no_browser_and_no_window_still_stops_cleanly(monkeypatch):
275
275
  ui.serve(port=0, open_browser=False, shell=True)
276
276
 
277
277
 
278
+ def test_a_browser_fallback_stops_once_the_tab_has_gone_quiet(monkeypatch):
279
+ """A packaged build has no console, so nothing else would ever stop it."""
280
+ server = threading.Thread(target=threading.Event().wait, daemon=True)
281
+ server.start()
282
+ clock = iter([100.0, 100.0, 104.0, 111.0])
283
+ monkeypatch.setattr(ui.time, "monotonic", lambda: next(clock))
284
+ ui._until_abandoned(server, quiet=10.0, poll=0.001)
285
+ assert server.is_alive(), "it stopped waiting; the server thread is a daemon"
286
+
287
+
288
+ def test_a_browser_fallback_keeps_serving_while_the_page_still_asks(monkeypatch):
289
+ server = threading.Thread(target=threading.Event().wait, daemon=True)
290
+ server.start()
291
+ times = iter([0.0, 5.0, 9.0, 14.0, 18.0, 30.0])
292
+ seen = iter([None, 8.0, 12.0, 17.0, None])
293
+
294
+ def now():
295
+ value = next(times)
296
+ heard = next(seen, None)
297
+ if heard is not None:
298
+ ui.Handler.last_seen = heard
299
+ return value
300
+
301
+ monkeypatch.setattr(ui.time, "monotonic", now)
302
+ ui._until_abandoned(server, quiet=10.0, poll=0.001)
303
+ # It ran to the last tick, which is the only one more than ten past a request.
304
+ assert next(times, "spent") == "spent"
305
+
306
+
307
+ def test_the_cli_can_open_the_window_a_build_opens(monkeypatch):
308
+ from shuttlecheck import cli
309
+ calls = {}
310
+ monkeypatch.setattr(cli, "serve", lambda root, **kw: calls.update(kw))
311
+ assert cli.main(["ui", "--window"]) == 0
312
+ assert calls["shell"] is True
313
+ assert cli.main(["ui"]) == 0
314
+ assert calls["shell"] is False
315
+ assert cli.main(["ui", "--window", "--no-browser"]) == 0
316
+ assert calls["shell"] is False, "--no-browser means nothing is shown at all"
317
+
318
+
278
319
  def test_serving_without_a_shell_is_unchanged(monkeypatch):
279
320
  """A checkout and an ingest box in a rack still get the old behaviour."""
280
321
  calls = []
@@ -1 +0,0 @@
1
- __version__ = "0.4.1"
File without changes
File without changes
File without changes
File without changes