collab-runtime 0.2.9__tar.gz → 0.3.0__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.2.9/collab_runtime.egg-info → collab_runtime-0.3.0}/PKG-INFO +4 -4
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/README.md +36 -16
- collab_runtime-0.3.0/collab/__init__.py +24 -0
- collab_runtime-0.3.0/collab/__main__.py +8 -0
- {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/dashboard/index.html +45 -65
- collab_runtime-0.3.0/collab/dashboard_server.py +171 -0
- collab_runtime-0.3.0/collab/errors.py +86 -0
- {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/live_locks_watcher.py +43 -190
- {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/lock_client.py +375 -543
- {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/logging_config.py +1 -1
- {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/main.py +13 -1
- collab_runtime-0.3.0/collab/platform_probe.py +305 -0
- collab_runtime-0.3.0/collab/safe_subprocess.py +313 -0
- collab_runtime-0.3.0/collab/subprocess_bridge.py +26 -0
- {collab_runtime-0.2.9 → collab_runtime-0.3.0/collab_runtime.egg-info}/PKG-INFO +4 -4
- collab_runtime-0.3.0/collab_runtime.egg-info/SOURCES.txt +22 -0
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/collab_runtime.egg-info/entry_points.txt +1 -1
- collab_runtime-0.3.0/collab_runtime.egg-info/top_level.txt +1 -0
- collab_runtime-0.2.9/README_pypi.md → collab_runtime-0.3.0/docs/pypi/README.md +3 -3
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/pyproject.toml +9 -12
- collab_runtime-0.2.9/collab/__init__.py +0 -77
- collab_runtime-0.2.9/collab/__main__.py +0 -11
- collab_runtime-0.2.9/collab_runtime.egg-info/SOURCES.txt +0 -86
- collab_runtime-0.2.9/collab_runtime.egg-info/top_level.txt +0 -10
- collab_runtime-0.2.9/scripts/cleanup.py +0 -395
- collab_runtime-0.2.9/scripts/collab_git_hook.py +0 -190
- collab_runtime-0.2.9/scripts/format_code.py +0 -594
- collab_runtime-0.2.9/scripts/generate_tests.py +0 -560
- collab_runtime-0.2.9/scripts/validate_code.py +0 -1397
- collab_runtime-0.2.9/src/__init__.py +0 -4
- collab_runtime-0.2.9/tests/backend/__init__.py +0 -0
- collab_runtime-0.2.9/tests/backend/functional/__init__.py +0 -0
- collab_runtime-0.2.9/tests/backend/functional/test_package_imports.py +0 -43
- collab_runtime-0.2.9/tests/backend/integration/__init__.py +0 -0
- collab_runtime-0.2.9/tests/backend/integration/test_cli_contract_parity.py +0 -220
- collab_runtime-0.2.9/tests/backend/performance/__init__.py +0 -0
- collab_runtime-0.2.9/tests/backend/reliability/__init__.py +0 -0
- collab_runtime-0.2.9/tests/backend/security/__init__.py +0 -0
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/__init__.py +0 -5
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/_helpers.py +0 -123
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/conftest.py +0 -18
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +0 -188
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +0 -56
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +0 -459
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +0 -1925
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +0 -187
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +0 -320
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +0 -67
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +0 -155
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +0 -684
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +0 -173
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +0 -71
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +0 -516
- collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +0 -296
- collab_runtime-0.2.9/tests/backend/unit/lock_client/__init__.py +0 -1
- collab_runtime-0.2.9/tests/backend/unit/lock_client/_helpers.py +0 -132
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_acquire.py +0 -214
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_active.py +0 -104
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_api.py +0 -63
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_cli.py +0 -682
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_daemon.py +0 -3730
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_dashboard.py +0 -438
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_discover.py +0 -241
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_force_release.py +0 -354
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_helper_branches.py +0 -1890
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_history.py +0 -301
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_isolation.py +0 -316
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_pid.py +0 -75
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_reconcile.py +0 -464
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_release.py +0 -77
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_shutdown.py +0 -1110
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_utils.py +0 -474
- collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_watch.py +0 -866
- collab_runtime-0.2.9/tests/backend/unit/scripts/__init__.py +0 -1
- collab_runtime-0.2.9/tests/backend/unit/scripts/_helpers.py +0 -42
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_cleanup.py +0 -285
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_collab_git_hook.py +0 -280
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_collab_git_hook_ported.py +0 -50
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_format_code.py +0 -368
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_format_code_ported.py +0 -177
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_generate_tests.py +0 -305
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_hook_templates.py +0 -357
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_setup_hook_overlay.py +0 -95
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_validate_code.py +0 -867
- collab_runtime-0.2.9/tests/backend/unit/scripts/test_validate_code_ported.py +0 -237
- collab_runtime-0.2.9/tests/backend/unit/test_entrypoints_main_run.py +0 -83
- collab_runtime-0.2.9/tests/backend/unit/test_logging_config.py +0 -529
- collab_runtime-0.2.9/tests/backend/unit/test_main_watch_pid_file.py +0 -278
- collab_runtime-0.2.9/tests/conftest.py +0 -167
- collab_runtime-0.2.9/tests/frontend/__init__.py +0 -0
- collab_runtime-0.2.9/tests/frontend/jest/__init__.py +0 -0
- collab_runtime-0.2.9/tests/frontend/playwright/__init__.py +0 -0
- collab_runtime-0.2.9/tests/packaging/test_smoke_install.py +0 -76
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/LICENSE +0 -0
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/collab_runtime.egg-info/dependency_links.txt +0 -0
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/collab_runtime.egg-info/requires.txt +0 -0
- {collab_runtime-0.2.9 → collab_runtime-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: collab-runtime
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Collaborative file locking runtime
|
|
5
5
|
Author-email: KirilMT <kiril.mt95@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -77,7 +77,7 @@ pip install "collab-runtime>=0.2.2"
|
|
|
77
77
|
|
|
78
78
|
### 1 — Create the Database Schema
|
|
79
79
|
|
|
80
|
-
In your Supabase project, open **SQL Editor** and run the contents of [`schema.sql`](https://github.com/KirilMT/collab/blob/main/schema.sql).
|
|
80
|
+
In your Supabase project, open **SQL Editor** and run the contents of [`supabase/schema.sql`](https://github.com/KirilMT/collab/blob/main/supabase/schema.sql).
|
|
81
81
|
|
|
82
82
|
This creates the `file_locks` table, `file_locks_history` audit table, the atomic `acquire_lock()` RPC, Row Level Security policies, and Realtime publication.
|
|
83
83
|
|
|
@@ -164,7 +164,7 @@ The optional VS Code extension provides lock-on-open warnings, a status bar indi
|
|
|
164
164
|
**Install from source** (extension is bundled in the [GitHub repository](https://github.com/KirilMT/collab)):
|
|
165
165
|
|
|
166
166
|
1. Press `F1` → **Developer: Install Extension from Location...**
|
|
167
|
-
2. Select the `vscode
|
|
167
|
+
2. Select the `editors/vscode/collab-locks/` directory
|
|
168
168
|
3. Reload VS Code
|
|
169
169
|
|
|
170
170
|
Once installed, the extension automatically starts and stops the background daemon with your VS Code window.
|
|
@@ -198,7 +198,7 @@ Lock release follows the same path in reverse and writes an entry to `file_locks
|
|
|
198
198
|
|
|
199
199
|
- Lock correctness is enforced at the database level via atomic RPC — no client-side race conditions.
|
|
200
200
|
- Force-release requires `SUPABASE_SERVICE_ROLE_KEY`; regular releases are scoped to the owning developer.
|
|
201
|
-
- Row Level Security (RLS) is configured on all tables via `schema.sql`.
|
|
201
|
+
- Row Level Security (RLS) is configured on all tables via `supabase/schema.sql`.
|
|
202
202
|
- Never commit secrets; use `.env` for local configuration only.
|
|
203
203
|
|
|
204
204
|
---
|
|
@@ -17,14 +17,14 @@ Prevents merge conflicts by automatically locking files when a developer starts
|
|
|
17
17
|
|
|
18
18
|
### 1. Create the Database Schema
|
|
19
19
|
|
|
20
|
-
Open your Supabase project's **SQL Editor** and run the contents of `schema.sql`:
|
|
20
|
+
Open your Supabase project's **SQL Editor** and run the contents of `supabase/schema.sql`:
|
|
21
21
|
|
|
22
22
|
**Steps:**
|
|
23
23
|
|
|
24
24
|
1. Open your Supabase project dashboard
|
|
25
25
|
2. Navigate to **SQL Editor** (left sidebar)
|
|
26
26
|
3. Click **New Query**
|
|
27
|
-
4. Copy-paste the full contents of [schema.sql](schema.sql) from this repository
|
|
27
|
+
4. Copy-paste the full contents of [supabase/schema.sql](supabase/schema.sql) from this repository
|
|
28
28
|
5. Click **Run**
|
|
29
29
|
|
|
30
30
|
This creates:
|
|
@@ -53,7 +53,7 @@ One command handles everything — dependencies, `.env` configuration, and IDE i
|
|
|
53
53
|
**Linux/macOS (Bash):**
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
./scripts/setup.sh
|
|
56
|
+
./scripts/setup-dev.sh
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
**End-User Install (via pip):**
|
|
@@ -102,8 +102,8 @@ Once setup is complete, test the locking system:
|
|
|
102
102
|
**Terminal 1 (Lock a file):**
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
collab acquire
|
|
106
|
-
collab status
|
|
105
|
+
collab acquire collab/main.py --reason "Testing locking system"
|
|
106
|
+
collab status collab/main.py
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
**Terminal 2 (View lock in another session):**
|
|
@@ -115,7 +115,7 @@ collab active
|
|
|
115
115
|
**Then release the lock:**
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
collab release
|
|
118
|
+
collab release collab/main.py
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
**View real-time lock changes:**
|
|
@@ -202,11 +202,11 @@ The extension is primarily distributed via the **Collab Runtime** Python package
|
|
|
202
202
|
**Manual Install (from source):**
|
|
203
203
|
|
|
204
204
|
1. Press `F1` -> `Developer: Install Extension from Location...`
|
|
205
|
-
2. Select `vscode
|
|
205
|
+
2. Select `editors/vscode/collab-locks/`
|
|
206
206
|
3. Reload VS Code
|
|
207
207
|
|
|
208
208
|
**CLI Install:**
|
|
209
|
-
|
|
209
|
+
Production `scripts/setup.ps1` / `scripts/setup.sh` download the latest release `.vsix` and install it when a supported editor CLI is on `PATH`. **Development setup** (`scripts/setup-dev.ps1` / `scripts/setup-dev.sh`) repeats this with stronger IDE detection and resolves `code` / `cursor` (and siblings) from common install locations on Windows and macOS when they are not on `PATH`, which fixes installs from integrated terminals (for example **Cursor**).
|
|
210
210
|
|
|
211
211
|
### Features
|
|
212
212
|
|
|
@@ -279,7 +279,7 @@ if (Test-Path $pidPath) {
|
|
|
279
279
|
|
|
280
280
|
## Database Schema
|
|
281
281
|
|
|
282
|
-
The full schema is in `schema.sql` and includes:
|
|
282
|
+
The full schema is in `supabase/schema.sql` and includes:
|
|
283
283
|
|
|
284
284
|
- `file_locks` for active locks.
|
|
285
285
|
- `file_locks_history` for audit/history.
|
|
@@ -302,7 +302,7 @@ The full schema is in `schema.sql` and includes:
|
|
|
302
302
|
|
|
303
303
|
```
|
|
304
304
|
collab/
|
|
305
|
-
├──
|
|
305
|
+
├── collab/
|
|
306
306
|
│ ├── lock_client.py # CLI entry point
|
|
307
307
|
│ ├── live_locks_watcher.py # Background watcher
|
|
308
308
|
│ ├── main.py # CLI orchestration + module entry point
|
|
@@ -315,18 +315,24 @@ collab/
|
|
|
315
315
|
│ │ ├── functional/ # Functional tests
|
|
316
316
|
│ │ ├── integration/ # Integration tests
|
|
317
317
|
│ │ ├── security/ # Security tests
|
|
318
|
-
│ │
|
|
319
|
-
│ │ └── reliability/ # Reliability tests
|
|
318
|
+
│ │ └── (performance/ and reliability/ removed — empty placeholders deleted for clean/optimized structure)
|
|
320
319
|
│ └── frontend/
|
|
321
|
-
│
|
|
322
|
-
│ └── playwright/ # Frontend e2e placeholder
|
|
320
|
+
│ └── playwright/ # E2E + visual regression (config, CI job, helpers, snapshots, deterministic fixtures)
|
|
323
321
|
├── scripts/
|
|
324
|
-
│ ├── setup-dev.ps1 # Windows setup
|
|
322
|
+
│ ├── setup-dev.ps1 # Windows dev setup
|
|
325
323
|
│ ├── setup.sh # Linux/macOS setup
|
|
324
|
+
│ ├── git-hooks/ # Collab git hook templates
|
|
325
|
+
│ ├── install_hooks.sh # Installs templates into .git/hooks
|
|
326
326
|
│ ├── format_code.py # Code formatter
|
|
327
327
|
│ ├── validate_code.py # CI validator
|
|
328
328
|
│ └── cleanup.py # Cache cleanup
|
|
329
|
-
├──
|
|
329
|
+
├── supabase/
|
|
330
|
+
│ └── schema.sql # Supabase database schema
|
|
331
|
+
├── editors/
|
|
332
|
+
│ ├── vscode/collab-locks/ # VS Code / Cursor extension
|
|
333
|
+
│ └── pycharm/ # PyCharm run configuration template
|
|
334
|
+
├── docs/
|
|
335
|
+
│ └── pypi/README.md # PyPI package readme
|
|
330
336
|
├── pyproject.toml # Package configuration
|
|
331
337
|
└── README.md # This file
|
|
332
338
|
```
|
|
@@ -463,6 +469,20 @@ MIT License — see LICENSE file for details.
|
|
|
463
469
|
|
|
464
470
|
---
|
|
465
471
|
|
|
472
|
+
## Documentation
|
|
473
|
+
|
|
474
|
+
| Document | Description |
|
|
475
|
+
| --------------------------------------------- | ---------------------------------------- |
|
|
476
|
+
| [ARCHITECTURE.md](docs/ARCHITECTURE.md) | System design and data flow |
|
|
477
|
+
| [API.md](docs/API.md) | CLI overview and environment variables |
|
|
478
|
+
| [CLI_REFERENCE.md](docs/CLI_REFERENCE.md) | Full command reference |
|
|
479
|
+
| [SECURITY.md](docs/SECURITY.md) | Subprocess hardening and secret handling |
|
|
480
|
+
| [PERFORMANCE.md](docs/PERFORMANCE.md) | Validation and watcher tuning |
|
|
481
|
+
| [TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md) | Common issues and fixes |
|
|
482
|
+
| [collab_roadmap.md](docs/collab_roadmap.md) | Future enhancements |
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
466
486
|
## Support
|
|
467
487
|
|
|
468
488
|
For issues, questions, or feature requests:
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Collab runtime package (published on PyPI as ``collab-runtime``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
__all__ = ["__version__"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _installed_version(dist_name: str = "collab-runtime") -> Optional[str]:
|
|
11
|
+
try:
|
|
12
|
+
from importlib.metadata import version as _ver
|
|
13
|
+
except Exception:
|
|
14
|
+
try:
|
|
15
|
+
from importlib_metadata import version as _ver # type: ignore
|
|
16
|
+
except Exception:
|
|
17
|
+
return None
|
|
18
|
+
try:
|
|
19
|
+
return _ver(dist_name)
|
|
20
|
+
except Exception:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
__version__ = _installed_version("collab-runtime") or "0.0.0"
|
|
@@ -489,53 +489,71 @@
|
|
|
489
489
|
<span>Collaborative Explorer</span>
|
|
490
490
|
</div>
|
|
491
491
|
<div class="nav-right">
|
|
492
|
-
<span id="last-update" class="chip">Not synced yet</span>
|
|
493
|
-
<span id="user-info" class="chip hidden"></span>
|
|
494
|
-
<button id="nav-locks"
|
|
495
|
-
|
|
496
|
-
|
|
492
|
+
<span id="last-update" class="chip" data-testid="last-update">Not synced yet</span>
|
|
493
|
+
<span id="user-info" class="chip hidden" data-testid="user-info"></span>
|
|
494
|
+
<button id="nav-locks"
|
|
495
|
+
class="btn btn-sm btn-primary"
|
|
496
|
+
type="button"
|
|
497
|
+
data-testid="nav-locks">Active Locks</button>
|
|
498
|
+
<button id="nav-history"
|
|
499
|
+
class="btn btn-sm btn-outline-primary"
|
|
500
|
+
type="button"
|
|
501
|
+
data-testid="nav-history">History Locks</button>
|
|
502
|
+
<button id="sync-btn"
|
|
503
|
+
class="btn btn-sm btn-outline-secondary"
|
|
504
|
+
type="button"
|
|
505
|
+
data-testid="sync-btn">
|
|
497
506
|
<i class="fas fa-sync-alt me-1"></i>Sync
|
|
498
507
|
</button>
|
|
499
508
|
</div>
|
|
500
509
|
</header>
|
|
501
510
|
<main class="app-main">
|
|
502
|
-
<section id="setup-view" class="setup-view hidden">
|
|
511
|
+
<section id="setup-view" class="setup-view hidden" data-testid="setup-view">
|
|
503
512
|
<h4 class="mb-2">Connect To Supabase</h4>
|
|
504
513
|
<p class="text-muted mb-3">Configure credentials in .env and restart the dashboard server.</p>
|
|
505
514
|
<pre class="bg-light border rounded p-3 mb-0">SUPABASE_URL=https://your-project.supabase.co
|
|
506
515
|
SUPABASE_ANON_KEY=your_anon_key</pre>
|
|
507
516
|
</section>
|
|
508
|
-
<section id="locks-page"
|
|
517
|
+
<section id="locks-page"
|
|
518
|
+
class="page-view hidden"
|
|
519
|
+
aria-label="locks view"
|
|
520
|
+
data-testid="locks-page">
|
|
509
521
|
<div class="page-head">
|
|
510
522
|
<h2 class="page-title">
|
|
511
523
|
<i class="fas fa-lock"></i>Active Locks
|
|
512
524
|
</h2>
|
|
513
525
|
</div>
|
|
514
|
-
<div class="stats-grid"
|
|
515
|
-
|
|
526
|
+
<div class="stats-grid"
|
|
527
|
+
aria-label="dashboard stats"
|
|
528
|
+
data-testid="stats-grid">
|
|
529
|
+
<article class="stat-card" data-testid="stat-card-active">
|
|
516
530
|
<div class="stat-shell">
|
|
517
531
|
<span class="stat-icon stat-icon-lock"><i class="fas fa-lock"></i></span>
|
|
518
532
|
<div>
|
|
519
533
|
<p class="stat-label">Active Locks</p>
|
|
520
|
-
<p class="stat-value" id="stat-active">0</p>
|
|
534
|
+
<p class="stat-value" id="stat-active" data-testid="stat-active">0</p>
|
|
521
535
|
</div>
|
|
522
536
|
</div>
|
|
523
537
|
</article>
|
|
524
|
-
<article class="stat-card">
|
|
538
|
+
<article class="stat-card" data-testid="stat-card-releases">
|
|
525
539
|
<div class="stat-shell">
|
|
526
540
|
<span class="stat-icon stat-icon-release"><i class="fas fa-check-double"></i></span>
|
|
527
541
|
<div>
|
|
528
542
|
<p class="stat-label">Releases Today</p>
|
|
529
|
-
<p class="stat-value stat-value-success"
|
|
543
|
+
<p class="stat-value stat-value-success"
|
|
544
|
+
id="stat-releases"
|
|
545
|
+
data-testid="stat-releases">0</p>
|
|
530
546
|
</div>
|
|
531
547
|
</div>
|
|
532
548
|
</article>
|
|
533
|
-
<article class="stat-card">
|
|
549
|
+
<article class="stat-card" data-testid="stat-card-avg">
|
|
534
550
|
<div class="stat-shell">
|
|
535
551
|
<span class="stat-icon stat-icon-avg"><i class="fas fa-bolt"></i></span>
|
|
536
552
|
<div>
|
|
537
553
|
<p class="stat-label">Avg Hold Time</p>
|
|
538
|
-
<p class="stat-value stat-value-info"
|
|
554
|
+
<p class="stat-value stat-value-info"
|
|
555
|
+
id="stat-avg"
|
|
556
|
+
data-testid="stat-avg">0m</p>
|
|
539
557
|
</div>
|
|
540
558
|
</div>
|
|
541
559
|
</article>
|
|
@@ -553,7 +571,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
|
|
|
553
571
|
<th class="text-end">Action</th>
|
|
554
572
|
</tr>
|
|
555
573
|
</thead>
|
|
556
|
-
<tbody id="active-locks-body">
|
|
574
|
+
<tbody id="active-locks-body" data-testid="active-locks-body">
|
|
557
575
|
<tr>
|
|
558
576
|
<td colspan="6" class="text-center py-4 text-muted">Connecting...</td>
|
|
559
577
|
</tr>
|
|
@@ -562,7 +580,10 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
|
|
|
562
580
|
</div>
|
|
563
581
|
</div>
|
|
564
582
|
</section>
|
|
565
|
-
<section id="history-page"
|
|
583
|
+
<section id="history-page"
|
|
584
|
+
class="page-view hidden"
|
|
585
|
+
aria-label="history view"
|
|
586
|
+
data-testid="history-page">
|
|
566
587
|
<div class="page-head">
|
|
567
588
|
<h2 class="page-title">
|
|
568
589
|
<i class="fas fa-clock-rotate-left"></i>Lock History
|
|
@@ -583,7 +604,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
|
|
|
583
604
|
<th>Outcome</th>
|
|
584
605
|
</tr>
|
|
585
606
|
</thead>
|
|
586
|
-
<tbody id="history-body">
|
|
607
|
+
<tbody id="history-body" data-testid="history-body">
|
|
587
608
|
<tr>
|
|
588
609
|
<td colspan="8" class="text-center py-4 text-muted">Loading history...</td>
|
|
589
610
|
</tr>
|
|
@@ -626,6 +647,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
|
|
|
626
647
|
</div>
|
|
627
648
|
</div>
|
|
628
649
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
650
|
+
<script src="dashboard-format.js"></script>
|
|
629
651
|
<script>
|
|
630
652
|
const serverCfg = window.__SUPABASE_CONFIG__ || {};
|
|
631
653
|
const SUPABASE_URL = serverCfg.url || "";
|
|
@@ -665,56 +687,14 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
|
|
|
665
687
|
}
|
|
666
688
|
}
|
|
667
689
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function formatTime24(dt) {
|
|
677
|
-
return dt.toLocaleTimeString([], {
|
|
678
|
-
hour: "2-digit",
|
|
679
|
-
minute: "2-digit",
|
|
680
|
-
hour12: false
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function formatDateTime24(dt) {
|
|
685
|
-
return formatDateLong(dt) + " " + formatTime24(dt);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function formatDurationMinutes(totalMinutes) {
|
|
689
|
-
const rounded = Math.max(0, Math.round(Number(totalMinutes) || 0));
|
|
690
|
-
if (!Number.isFinite(rounded) || rounded <= 0) {
|
|
691
|
-
return "0m";
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const units = [
|
|
695
|
-
{ label: "mo", minutes: 30 * 24 * 60 },
|
|
696
|
-
{ label: "d", minutes: 24 * 60 },
|
|
697
|
-
{ label: "h", minutes: 60 },
|
|
698
|
-
{ label: "m", minutes: 1 }
|
|
699
|
-
];
|
|
700
|
-
|
|
701
|
-
let remaining = rounded;
|
|
702
|
-
const parts = [];
|
|
703
|
-
|
|
704
|
-
units.forEach((unit) => {
|
|
705
|
-
if (remaining >= unit.minutes) {
|
|
706
|
-
const value = Math.floor(remaining / unit.minutes);
|
|
707
|
-
remaining -= value * unit.minutes;
|
|
708
|
-
parts.push(String(value) + unit.label);
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
return parts.length ? parts.join(" ") : "0m";
|
|
713
|
-
}
|
|
690
|
+
const fmt = window.DashboardFormat;
|
|
691
|
+
const formatDateLong = fmt.formatDateLong;
|
|
692
|
+
const formatTime24 = fmt.formatTime24;
|
|
693
|
+
const formatDateTime24 = fmt.formatDateTime24;
|
|
694
|
+
const formatDurationMinutes = fmt.formatDurationMinutes;
|
|
714
695
|
|
|
715
696
|
function routeFromHash() {
|
|
716
|
-
|
|
717
|
-
return h === "history" ? "history" : "locks";
|
|
697
|
+
return fmt.routeFromHash(window.location.hash);
|
|
718
698
|
}
|
|
719
699
|
|
|
720
700
|
function navigate(page) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Local HTTP server for the collaborative dashboard static assets.
|
|
2
|
+
|
|
3
|
+
The dashboard HTML references sibling static files (e.g. ``dashboard-format.js``). The
|
|
4
|
+
injected config HTML must therefore be written *inside* ``collab/dashboard/`` and served
|
|
5
|
+
from that directory — not from a lone temp file in ``/tmp``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import atexit
|
|
11
|
+
import http.server
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import tempfile
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
from functools import partial
|
|
19
|
+
from typing import Any, Callable, Optional, Tuple
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
DASHBOARD_TEMP_PREFIX = ".collab-dashboard-"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _QuietDashboardHandler(http.server.SimpleHTTPRequestHandler):
|
|
27
|
+
"""Serve dashboard static files without per-request stderr logging."""
|
|
28
|
+
|
|
29
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
30
|
+
"""Suppress default SimpleHTTPRequestHandler request log lines."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def dashboard_directory(resource_root: str) -> str:
|
|
34
|
+
"""Return the path to packaged dashboard static assets."""
|
|
35
|
+
return os.path.join(resource_root, "dashboard")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_dashboard_template(resource_root: str) -> Optional[str]:
|
|
39
|
+
"""Read ``index.html`` from the dashboard package directory."""
|
|
40
|
+
html_path = os.path.join(dashboard_directory(resource_root), "index.html")
|
|
41
|
+
if not os.path.exists(html_path):
|
|
42
|
+
logger.error("Dashboard file not found at %s", html_path)
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
with open(html_path, "r", encoding="utf-8") as fh:
|
|
46
|
+
return fh.read()
|
|
47
|
+
except OSError as exc:
|
|
48
|
+
logger.error("Error reading dashboard template: %s", exc)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def write_injected_dashboard_html(
|
|
53
|
+
resource_root: str, injected: dict[str, Any]
|
|
54
|
+
) -> Optional[str]:
|
|
55
|
+
"""Write config-injected HTML next to static assets; return path or None."""
|
|
56
|
+
content = read_dashboard_template(resource_root)
|
|
57
|
+
if content is None:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
dash_dir = dashboard_directory(resource_root)
|
|
61
|
+
inject_script = (
|
|
62
|
+
f"<script>window.__SUPABASE_CONFIG__ = {json.dumps(injected)};</script>\n"
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
tmp = tempfile.NamedTemporaryFile(
|
|
66
|
+
mode="w",
|
|
67
|
+
delete=False,
|
|
68
|
+
suffix=".html",
|
|
69
|
+
prefix=DASHBOARD_TEMP_PREFIX,
|
|
70
|
+
dir=dash_dir,
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
)
|
|
73
|
+
tmp.write(inject_script)
|
|
74
|
+
tmp.write(content)
|
|
75
|
+
tmp.flush()
|
|
76
|
+
tmp.close()
|
|
77
|
+
return tmp.name
|
|
78
|
+
except OSError as exc:
|
|
79
|
+
logger.error("Error creating temp dashboard file: %s", exc)
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _register_temp_html_cleanup(html_path: str) -> None:
|
|
84
|
+
"""Remove generated dashboard HTML on process exit."""
|
|
85
|
+
|
|
86
|
+
def _unlink() -> None:
|
|
87
|
+
try:
|
|
88
|
+
os.unlink(html_path)
|
|
89
|
+
except OSError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
atexit.register(_unlink)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def start_dashboard_http_server(
|
|
96
|
+
resource_root: str,
|
|
97
|
+
injected_html_path: str,
|
|
98
|
+
*,
|
|
99
|
+
log_error: Callable[[str, Any], None] = logger.error,
|
|
100
|
+
log_warning: Callable[[str, Any], None] = logger.warning,
|
|
101
|
+
) -> Optional[str]:
|
|
102
|
+
"""Serve ``collab/dashboard`` and return the URL to the injected HTML file."""
|
|
103
|
+
dash_dir = dashboard_directory(resource_root)
|
|
104
|
+
filename = os.path.basename(injected_html_path)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
handler = partial(_QuietDashboardHandler, directory=dash_dir)
|
|
108
|
+
|
|
109
|
+
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
110
|
+
port = server.server_address[1]
|
|
111
|
+
|
|
112
|
+
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
113
|
+
thread.start()
|
|
114
|
+
|
|
115
|
+
def _safe_shutdown() -> None:
|
|
116
|
+
try:
|
|
117
|
+
server.shutdown()
|
|
118
|
+
except BaseException:
|
|
119
|
+
pass
|
|
120
|
+
close = getattr(server, "server_close", None)
|
|
121
|
+
if callable(close):
|
|
122
|
+
try:
|
|
123
|
+
close()
|
|
124
|
+
except OSError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
atexit.register(_safe_shutdown)
|
|
128
|
+
_register_temp_html_cleanup(injected_html_path)
|
|
129
|
+
|
|
130
|
+
url = f"http://127.0.0.1:{port}/{filename}"
|
|
131
|
+
|
|
132
|
+
import socket
|
|
133
|
+
|
|
134
|
+
for _ in range(20):
|
|
135
|
+
try:
|
|
136
|
+
with socket.create_connection(("127.0.0.1", port), timeout=0.3):
|
|
137
|
+
break
|
|
138
|
+
except OSError:
|
|
139
|
+
time.sleep(0.05)
|
|
140
|
+
|
|
141
|
+
return url
|
|
142
|
+
except OSError as exc:
|
|
143
|
+
log_error("Failed to start local dashboard server: %s", exc)
|
|
144
|
+
try:
|
|
145
|
+
os.unlink(injected_html_path)
|
|
146
|
+
except OSError as cleanup_exc:
|
|
147
|
+
log_warning("Dashboard temp-file cleanup failed: %s", cleanup_exc)
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def prepare_dashboard_server(
|
|
152
|
+
resource_root: str,
|
|
153
|
+
injected: dict[str, Any],
|
|
154
|
+
*,
|
|
155
|
+
log_error: Callable[[str, Any], None] = logger.error,
|
|
156
|
+
log_warning: Callable[[str, Any], None] = logger.warning,
|
|
157
|
+
) -> Tuple[Optional[str], Optional[str]]:
|
|
158
|
+
"""Write injected HTML, start server from dashboard dir; return (url, path)."""
|
|
159
|
+
html_path = write_injected_dashboard_html(resource_root, injected)
|
|
160
|
+
if not html_path:
|
|
161
|
+
return None, None
|
|
162
|
+
|
|
163
|
+
url = start_dashboard_http_server(
|
|
164
|
+
resource_root,
|
|
165
|
+
html_path,
|
|
166
|
+
log_error=log_error,
|
|
167
|
+
log_warning=log_warning,
|
|
168
|
+
)
|
|
169
|
+
if not url:
|
|
170
|
+
return None, None
|
|
171
|
+
return url, html_path
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Structured error taxonomy for collab runtime lifecycle paths.
|
|
2
|
+
|
|
3
|
+
Phase 5 (hardening) introduces stable exception types so daemon, watcher, PID, and
|
|
4
|
+
subprocess failures can be classified without string matching in tests or operators.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CollabError(Exception):
|
|
13
|
+
"""Base exception for collab runtime failures."""
|
|
14
|
+
|
|
15
|
+
code: str = "collab_error"
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str, *, detail: Optional[str] = None) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.message = message
|
|
20
|
+
self.detail = detail
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ConfigurationError(CollabError):
|
|
24
|
+
"""Missing or invalid runtime configuration (credentials, client, env)."""
|
|
25
|
+
|
|
26
|
+
code = "configuration_error"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LockServiceUnavailableError(CollabError):
|
|
30
|
+
"""Lock service (Supabase) cannot be reached — DNS, network, or API outage."""
|
|
31
|
+
|
|
32
|
+
code = "lock_service_unavailable"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DaemonLifecycleError(CollabError):
|
|
36
|
+
"""Daemon / background watcher lifecycle failure."""
|
|
37
|
+
|
|
38
|
+
code = "daemon_lifecycle_error"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DaemonStartError(DaemonLifecycleError):
|
|
42
|
+
"""Watcher daemon failed to start or record a healthy PID."""
|
|
43
|
+
|
|
44
|
+
code = "daemon_start_error"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DaemonStopError(DaemonLifecycleError):
|
|
48
|
+
"""Watcher daemon failed to stop cleanly."""
|
|
49
|
+
|
|
50
|
+
code = "daemon_stop_error"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class PidError(CollabError):
|
|
54
|
+
"""PID file or process identifier handling failure."""
|
|
55
|
+
|
|
56
|
+
code = "pid_error"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PidParseError(PidError):
|
|
60
|
+
"""PID file contents are missing or malformed."""
|
|
61
|
+
|
|
62
|
+
code = "pid_parse_error"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class WatcherDiscoveryError(CollabError):
|
|
66
|
+
"""Could not discover or verify an existing watcher process."""
|
|
67
|
+
|
|
68
|
+
code = "watcher_discovery_error"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ParentMonitorError(CollabError):
|
|
72
|
+
"""Parent IDE / terminal process monitoring failure."""
|
|
73
|
+
|
|
74
|
+
code = "parent_monitor_error"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SubprocessSecurityError(CollabError):
|
|
78
|
+
"""Rejected subprocess invocation (unknown executable or disallowed args)."""
|
|
79
|
+
|
|
80
|
+
code = "subprocess_security_error"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SubprocessExecutionError(CollabError):
|
|
84
|
+
"""Subprocess exited with failure or timed out."""
|
|
85
|
+
|
|
86
|
+
code = "subprocess_execution_error"
|