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.
- {collab_runtime-0.3.0/collab_runtime.egg-info → collab_runtime-0.3.1}/PKG-INFO +1 -1
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/dashboard/index.html +93 -33
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/dashboard_server.py +79 -8
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/live_locks_watcher.py +1 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/lock_client.py +3 -1
- {collab_runtime-0.3.0 → collab_runtime-0.3.1/collab_runtime.egg-info}/PKG-INFO +1 -1
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/pyproject.toml +1 -1
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/LICENSE +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/README.md +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/__init__.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/__main__.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/errors.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/logging_config.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/main.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/platform_probe.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/safe_subprocess.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab/subprocess_bridge.py +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/SOURCES.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/dependency_links.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/entry_points.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/requires.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/collab_runtime.egg-info/top_level.txt +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/docs/pypi/README.md +0 -0
- {collab_runtime-0.3.0 → collab_runtime-0.3.1}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
|
814
|
+
function loadSupabaseLibrary() {
|
|
767
815
|
return new Promise((resolve, reject) => {
|
|
768
816
|
if (window.supabase) {
|
|
769
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
"""
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
)
|
|
@@ -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(
|
|
2093
|
+
return prepare_dashboard_server(
|
|
2094
|
+
_RESOURCE_ROOT, injected, project_root=_PROJECT_ROOT
|
|
2095
|
+
)
|
|
2094
2096
|
|
|
2095
2097
|
# ------------------------------------------------------------------
|
|
2096
2098
|
# Watcher (foreground process)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|