screengraft 0.62.0 → 0.62.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screengraft",
3
- "version": "0.62.0",
3
+ "version": "0.62.2",
4
4
  "description": "Put a UI screenshot or screen recording onto a photographed device screen with the perspective exactly right \u2014 a homography you confirm by hand, not a generative guess.",
5
5
  "keywords": [
6
6
  "mockup",
@@ -39,7 +39,16 @@ def _load() -> dict:
39
39
  if not isinstance(d, dict):
40
40
  raise ValueError
41
41
  except (OSError, ValueError):
42
+ # No store (or an unreadable one): build it from the sessions on disk
43
+ # and WRITE it, or every read until the first record() walks them again.
42
44
  d = _seed()
45
+ for r in ROLES:
46
+ d.setdefault(r, [])
47
+ try:
48
+ _save(d)
49
+ except OSError:
50
+ pass
51
+ return d
43
52
  for r in ROLES:
44
53
  d.setdefault(r, [])
45
54
  return d
@@ -52,6 +61,10 @@ def _save(d: dict):
52
61
  with open(tmp, "w") as f:
53
62
  json.dump(d, f, indent=1)
54
63
  os.replace(tmp, p)
64
+ try:
65
+ os.chmod(p, 0o600) # paths under the user's home: owner-only
66
+ except OSError:
67
+ pass
55
68
 
56
69
 
57
70
  def _seed() -> dict:
package/scripts/ui.py CHANGED
@@ -28,6 +28,7 @@ stdlib only on the server side; OpenCV via detect/warp.
28
28
  """
29
29
  import argparse
30
30
  import atexit
31
+ import hashlib
31
32
  import json
32
33
  import mimetypes
33
34
  import os
@@ -733,7 +734,10 @@ def _thumb_for(item):
733
734
  out_dir = os.path.join(SESSION.dir, "thumbs")
734
735
  if item.get("ext") in VIDEO_EXT:
735
736
  os.makedirs(out_dir, exist_ok=True)
736
- out = os.path.join(out_dir, f"{abs(hash(item['path']))}_{int(item.get('mtime') or 0)}_clip.jpg")
737
+ # A stable name: hash() is salted per process, so a name built on it
738
+ # misses the cache on every launch.
739
+ key = hashlib.sha1(item["path"].encode()).hexdigest()[:16]
740
+ out = os.path.join(out_dir, f"{key}_{int(item.get('mtime') or 0)}_clip.jpg")
737
741
  if not os.path.exists(out):
738
742
  try:
739
743
  fr = W.read_frame_at(item["path"], 0)
@@ -981,7 +985,10 @@ class Handler(BaseHTTPRequestHandler):
981
985
  # The folder scan (scan.py) still exists for the CLI and for a
982
986
  # first run with nothing used yet; ?role= selects the list.
983
987
  role = (q.get("role") or ["photo"])[0]
984
- limit = int(q.get("limit", ["40"])[0])
988
+ try:
989
+ limit = max(1, min(200, int(q.get("limit", ["40"])[0])))
990
+ except ValueError:
991
+ limit = 40
985
992
  items = RECENT.items(role, limit)
986
993
  for it in items:
987
994
  it["thumb"] = _thumb_for(it)
@@ -1546,6 +1553,11 @@ def _publish_current(payload):
1546
1553
  """
1547
1554
  os.makedirs(os.path.dirname(CURRENT), exist_ok=True)
1548
1555
  _write_json_atomic(CURRENT, payload)
1556
+ # The pointer carries the session token: owner-only, like a credential.
1557
+ try:
1558
+ os.chmod(CURRENT, 0o600)
1559
+ except OSError:
1560
+ pass
1549
1561
 
1550
1562
  def clear():
1551
1563
  cur = None
package/ui/index.html CHANGED
@@ -461,7 +461,12 @@
461
461
  make it visible", 12 Sep 2026). Its own keyboard stop with its own name
462
462
  ("Remove photo"), distinct from the fit bar's Reset -- that resets the
463
463
  QUAD; this un-chooses a SOURCE. Only present while the chip is filled. */
464
- .chipwrap{position:relative;display:inline-flex;min-width:0}
464
+ /* Above the scrim (z 50) and beside the popover (z 51): the chip whose
465
+ popover is open stays visible and lit, so it is clear which source is
466
+ being chosen; the other chip stays clickable to switch (14 Sep 2026). The
467
+ top bar creates no stacking context of its own, so this z-index reaches
468
+ the root. */
469
+ .chipwrap{position:relative;display:inline-flex;min-width:0;z-index:52}
465
470
  .chipwrap .inputchip:not(.is-empty){padding-right:30px}
466
471
  .chipclear{position:absolute;right:5px;top:50%;transform:translateY(-50%);
467
472
  width:22px;height:22px;padding:0;border-radius:999px;flex:none;