collab-runtime 0.3.0__tar.gz → 0.3.1__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 (24) hide show
  1. {collab_runtime-0.3.0/collab_runtime.egg-info → collab_runtime-0.3.1}/PKG-INFO +1 -1
  2. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/dashboard/index.html +93 -33
  3. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/dashboard_server.py +79 -8
  4. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/live_locks_watcher.py +1 -0
  5. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/lock_client.py +3 -1
  6. {collab_runtime-0.3.0 → collab_runtime-0.3.1/collab_runtime.egg-info}/PKG-INFO +1 -1
  7. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/pyproject.toml +1 -1
  8. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/LICENSE +0 -0
  9. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/README.md +0 -0
  10. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/__init__.py +0 -0
  11. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/__main__.py +0 -0
  12. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/errors.py +0 -0
  13. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/logging_config.py +0 -0
  14. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/main.py +0 -0
  15. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/platform_probe.py +0 -0
  16. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/safe_subprocess.py +0 -0
  17. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/subprocess_bridge.py +0 -0
  18. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/SOURCES.txt +0 -0
  19. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/dependency_links.txt +0 -0
  20. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/entry_points.txt +0 -0
  21. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/requires.txt +0 -0
  22. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/top_level.txt +0 -0
  23. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/docs/pypi/README.md +0 -0
  24. {collab_runtime-0.3.0 → collab_runtime-0.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -490,6 +490,9 @@
490
490
  </div>
491
491
  <div class="nav-right">
492
492
  <span id="last-update" class="chip" data-testid="last-update">Not synced yet</span>
493
+ <span id="project-info"
494
+ class="chip hidden text-muted small"
495
+ data-testid="project-info"></span>
493
496
  <span id="user-info" class="chip hidden" data-testid="user-info"></span>
494
497
  <button id="nav-locks"
495
498
  class="btn btn-sm btn-primary"
@@ -649,12 +652,14 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
649
652
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
650
653
  <script src="dashboard-format.js"></script>
651
654
  <script>
652
- const serverCfg = window.__SUPABASE_CONFIG__ || {};
653
- const SUPABASE_URL = serverCfg.url || "";
654
- const SUPABASE_KEY = serverCfg.serviceKey || serverCfg.anonKey || "";
655
- const SUPABASE_USER = serverCfg.user || null;
656
- const IS_ADMIN = !!serverCfg.serviceKey;
657
- const SUPABASE_MODE = !!(SUPABASE_URL && SUPABASE_KEY);
655
+ let runtimeCfg = window.__SUPABASE_CONFIG__ || {};
656
+ let SUPABASE_URL = "";
657
+ let SUPABASE_KEY = "";
658
+ let SUPABASE_USER = null;
659
+ let IS_ADMIN = false;
660
+ let SUPABASE_MODE = false;
661
+ let supabaseClientFingerprint = "";
662
+ const RUNTIME_CONFIG_PATH = "/collab-runtime-config.json";
658
663
 
659
664
  const PAGE_SIZE = 25;
660
665
  const HISTORY_PREFETCH_GAP_PX = 120;
@@ -744,14 +749,57 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
744
749
  document.getElementById("nav-history").disabled = true;
745
750
  }
746
751
 
752
+ function applyRuntimeConfig() {
753
+ SUPABASE_URL = runtimeCfg.url || "";
754
+ SUPABASE_KEY = runtimeCfg.serviceKey || runtimeCfg.anonKey || "";
755
+ SUPABASE_USER = runtimeCfg.user || null;
756
+ IS_ADMIN = !!runtimeCfg.serviceKey;
757
+ SUPABASE_MODE = !!(SUPABASE_URL && SUPABASE_KEY);
758
+ updateProjectInfo();
759
+ }
760
+
761
+ function updateProjectInfo() {
762
+ const el = document.getElementById("project-info");
763
+ if (!SUPABASE_URL) {
764
+ el.classList.add("hidden");
765
+ el.textContent = "";
766
+ return;
767
+ }
768
+ try {
769
+ const host = new URL(SUPABASE_URL).hostname.replace(".supabase.co", "");
770
+ el.textContent = host;
771
+ el.title = "Supabase project: " + SUPABASE_URL;
772
+ el.classList.remove("hidden");
773
+ } catch (e) {
774
+ el.classList.add("hidden");
775
+ }
776
+ }
777
+
778
+ async function syncRuntimeConfig() {
779
+ try {
780
+ const resp = await fetch(RUNTIME_CONFIG_PATH, { cache: "no-store" });
781
+ if (resp.ok) {
782
+ const live = await resp.json();
783
+ if (live && live.url) {
784
+ runtimeCfg = live;
785
+ }
786
+ }
787
+ } catch (e) {
788
+ console.debug("Runtime config fetch skipped", e);
789
+ }
790
+ applyRuntimeConfig();
791
+ }
792
+
747
793
  function showMain() {
748
794
  document.getElementById("setup-view").classList.add("hidden");
749
795
  document.getElementById("locks-page").classList.remove("hidden");
750
796
  document.getElementById("history-page").classList.remove("hidden");
797
+ const userInfo = document.getElementById("user-info");
751
798
  if (SUPABASE_USER) {
752
- const userInfo = document.getElementById("user-info");
753
799
  userInfo.classList.remove("hidden");
754
800
  userInfo.innerHTML = '<i class="fab fa-github me-1"></i>' + SUPABASE_USER;
801
+ } else {
802
+ userInfo.classList.add("hidden");
755
803
  }
756
804
  }
757
805
 
@@ -763,33 +811,40 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
763
811
  '</td></tr>';
764
812
  }
765
813
 
766
- function loadSupabaseClient() {
814
+ function loadSupabaseLibrary() {
767
815
  return new Promise((resolve, reject) => {
768
816
  if (window.supabase) {
769
- try {
770
- supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
771
- resolve();
772
- } catch (e) {
773
- reject(e);
774
- }
817
+ resolve();
775
818
  return;
776
819
  }
777
-
778
820
  const script = document.createElement("script");
779
821
  script.src = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js/dist/umd/supabase.min.js";
780
- script.onload = () => {
781
- try {
782
- supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
783
- resolve();
784
- } catch (e) {
785
- reject(e);
786
- }
787
- };
822
+ script.onload = () => resolve();
788
823
  script.onerror = reject;
789
824
  document.head.appendChild(script);
790
825
  });
791
826
  }
792
827
 
828
+ function buildSupabaseClient() {
829
+ return window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
830
+ }
831
+
832
+ async function ensureSupabaseClient() {
833
+ await syncRuntimeConfig();
834
+ if (!SUPABASE_MODE) {
835
+ return false;
836
+ }
837
+ const fingerprint =
838
+ SUPABASE_URL + "|" + (runtimeCfg.serviceKey ? "service" : "anon");
839
+ if (supabaseClient && supabaseClientFingerprint === fingerprint) {
840
+ return true;
841
+ }
842
+ await loadSupabaseLibrary();
843
+ supabaseClient = buildSupabaseClient();
844
+ supabaseClientFingerprint = fingerprint;
845
+ return true;
846
+ }
847
+
793
848
  function subscribeRealtime() {
794
849
  if (!supabaseClient) {
795
850
  return;
@@ -820,6 +875,10 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
820
875
  const syncBtn = document.getElementById("sync-btn");
821
876
  syncBtn.disabled = true;
822
877
  try {
878
+ if (!(await ensureSupabaseClient())) {
879
+ showSetup();
880
+ return;
881
+ }
823
882
  const { data, error } = await supabaseClient
824
883
  .from("file_locks")
825
884
  .select("*")
@@ -844,7 +903,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
844
903
  console.error(e);
845
904
  showLocksError("Unable to fetch active locks.");
846
905
  } finally {
847
- syncBtn.disabled = false;
906
+ syncBtn.disabled = !SUPABASE_MODE;
848
907
  }
849
908
  }
850
909
 
@@ -929,6 +988,9 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
929
988
 
930
989
  historyLoading = true;
931
990
  try {
991
+ if (!(await ensureSupabaseClient())) {
992
+ return;
993
+ }
932
994
  const from = historyOffset;
933
995
  const to = historyOffset + PAGE_SIZE - 1;
934
996
  const { data, error } = await supabaseClient
@@ -1082,17 +1144,15 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
1082
1144
  async function init() {
1083
1145
  releaseModal = new bootstrap.Modal(document.getElementById("releaseModal"));
1084
1146
  wireEvents();
1085
-
1086
- if (!SUPABASE_MODE) {
1087
- showSetup();
1088
- return;
1089
- }
1090
-
1091
- showMain();
1092
- applyRoute();
1147
+ applyRuntimeConfig();
1093
1148
 
1094
1149
  try {
1095
- await loadSupabaseClient();
1150
+ if (!(await ensureSupabaseClient())) {
1151
+ showSetup();
1152
+ return;
1153
+ }
1154
+ showMain();
1155
+ applyRoute();
1096
1156
  await refreshLocks();
1097
1157
  if (routeFromHash() === "history") {
1098
1158
  resetHistory();
@@ -15,19 +15,87 @@ import os
15
15
  import tempfile
16
16
  import threading
17
17
  import time
18
- from functools import partial
18
+ import urllib.parse
19
19
  from typing import Any, Callable, Optional, Tuple
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
23
23
  DASHBOARD_TEMP_PREFIX = ".collab-dashboard-"
24
+ RUNTIME_CONFIG_PATH = "/collab-runtime-config.json"
24
25
 
25
26
 
26
- class _QuietDashboardHandler(http.server.SimpleHTTPRequestHandler):
27
- """Serve dashboard static files without per-request stderr logging."""
27
+ def load_runtime_supabase_config(project_root: str) -> dict[str, Any]:
28
+ """Read ``.env`` from *project_root* and return dashboard Supabase settings.
28
29
 
29
- def log_message(self, format: str, *args: Any) -> None:
30
- """Suppress default SimpleHTTPRequestHandler request log lines."""
30
+ The local dashboard fetches this on each sync so credential changes take effect
31
+ without restarting the watcher or reopening a stale browser tab. The project
32
+ ``.env`` is read directly (not via ``load_dotenv``) so polling never mutates the
33
+ running watcher's process environment.
34
+ """
35
+ from dotenv import dotenv_values
36
+
37
+ env_path = os.path.join(project_root, ".env")
38
+ file_vals: dict[str, Any] = {}
39
+ if os.path.isfile(env_path):
40
+ try:
41
+ file_vals = dict(dotenv_values(env_path))
42
+ except OSError as exc:
43
+ logger.debug("Could not read %s for runtime config: %s", env_path, exc)
44
+
45
+ def pick(name: str) -> Optional[str]:
46
+ val = file_vals.get(name)
47
+ if not val:
48
+ val = os.getenv(name)
49
+ return val
50
+
51
+ url = pick("SUPABASE_URL") or ""
52
+ anon = pick("SUPABASE_ANON_KEY") or ""
53
+ service = pick("SUPABASE_SERVICE_ROLE_KEY") or None
54
+ user = (
55
+ pick("COLLAB_DEVELOPER_ID")
56
+ or pick("DEVELOPER_ID")
57
+ or os.getenv("USERNAME")
58
+ or os.getenv("USER")
59
+ or ""
60
+ )
61
+ return {
62
+ "url": url,
63
+ "anonKey": anon,
64
+ "serviceKey": service,
65
+ "user": user,
66
+ }
67
+
68
+
69
+ def create_dashboard_handler(project_root: str, directory: str) -> type:
70
+ """Build a request handler that serves static assets and live runtime config."""
71
+
72
+ class DashboardHandler(http.server.SimpleHTTPRequestHandler):
73
+ """Serve dashboard static files without per-request stderr logging."""
74
+
75
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
76
+ super().__init__(*args, directory=directory, **kwargs)
77
+
78
+ def log_message(self, format: str, *args: Any) -> None:
79
+ """Suppress default SimpleHTTPRequestHandler request log lines."""
80
+
81
+ def do_GET(self) -> None:
82
+ parsed = urllib.parse.urlparse(self.path)
83
+ if parsed.path == RUNTIME_CONFIG_PATH:
84
+ self._serve_runtime_config()
85
+ return
86
+ super().do_GET()
87
+
88
+ def _serve_runtime_config(self) -> None:
89
+ payload = load_runtime_supabase_config(project_root)
90
+ body = json.dumps(payload).encode("utf-8")
91
+ self.send_response(200)
92
+ self.send_header("Content-Type", "application/json; charset=utf-8")
93
+ self.send_header("Content-Length", str(len(body)))
94
+ self.send_header("Cache-Control", "no-store")
95
+ self.end_headers()
96
+ self.wfile.write(body)
97
+
98
+ return DashboardHandler
31
99
 
32
100
 
33
101
  def dashboard_directory(resource_root: str) -> str:
@@ -96,17 +164,18 @@ def start_dashboard_http_server(
96
164
  resource_root: str,
97
165
  injected_html_path: str,
98
166
  *,
167
+ project_root: Optional[str] = None,
99
168
  log_error: Callable[[str, Any], None] = logger.error,
100
169
  log_warning: Callable[[str, Any], None] = logger.warning,
101
170
  ) -> Optional[str]:
102
171
  """Serve ``collab/dashboard`` and return the URL to the injected HTML file."""
103
172
  dash_dir = dashboard_directory(resource_root)
104
173
  filename = os.path.basename(injected_html_path)
174
+ env_root = project_root or resource_root
105
175
 
106
176
  try:
107
- handler = partial(_QuietDashboardHandler, directory=dash_dir)
108
-
109
- server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
177
+ handler_cls = create_dashboard_handler(env_root, dash_dir)
178
+ server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler_cls)
110
179
  port = server.server_address[1]
111
180
 
112
181
  thread = threading.Thread(target=server.serve_forever, daemon=True)
@@ -152,6 +221,7 @@ def prepare_dashboard_server(
152
221
  resource_root: str,
153
222
  injected: dict[str, Any],
154
223
  *,
224
+ project_root: Optional[str] = None,
155
225
  log_error: Callable[[str, Any], None] = logger.error,
156
226
  log_warning: Callable[[str, Any], None] = logger.warning,
157
227
  ) -> Tuple[Optional[str], Optional[str]]:
@@ -163,6 +233,7 @@ def prepare_dashboard_server(
163
233
  url = start_dashboard_http_server(
164
234
  resource_root,
165
235
  html_path,
236
+ project_root=project_root,
166
237
  log_error=log_error,
167
238
  log_warning=log_warning,
168
239
  )
@@ -707,6 +707,7 @@ def _start_dashboard_server() -> str | None:
707
707
  url, _html_path = prepare_dashboard_server(
708
708
  _RESOURCE_ROOT,
709
709
  injected,
710
+ project_root=_PROJECT_ROOT,
710
711
  log_error=logger.warning,
711
712
  log_warning=logger.debug,
712
713
  )
@@ -2090,7 +2090,9 @@ class LockClient:
2090
2090
  "serviceKey": SUPABASE_SERVICE_ROLE_KEY or None,
2091
2091
  "user": self.developer_id or "",
2092
2092
  }
2093
- return prepare_dashboard_server(_RESOURCE_ROOT, injected)
2093
+ return prepare_dashboard_server(
2094
+ _RESOURCE_ROOT, injected, project_root=_PROJECT_ROOT
2095
+ )
2094
2096
 
2095
2097
  # ------------------------------------------------------------------
2096
2098
  # Watcher (foreground process)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "collab-runtime"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Collaborative file locking runtime"
9
9
  readme = "docs/pypi/README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes