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.
Files changed (97) hide show
  1. {collab_runtime-0.2.9/collab_runtime.egg-info → collab_runtime-0.3.0}/PKG-INFO +4 -4
  2. {collab_runtime-0.2.9 → collab_runtime-0.3.0}/README.md +36 -16
  3. collab_runtime-0.3.0/collab/__init__.py +24 -0
  4. collab_runtime-0.3.0/collab/__main__.py +8 -0
  5. {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/dashboard/index.html +45 -65
  6. collab_runtime-0.3.0/collab/dashboard_server.py +171 -0
  7. collab_runtime-0.3.0/collab/errors.py +86 -0
  8. {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/live_locks_watcher.py +43 -190
  9. {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/lock_client.py +375 -543
  10. {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/logging_config.py +1 -1
  11. {collab_runtime-0.2.9/src → collab_runtime-0.3.0/collab}/main.py +13 -1
  12. collab_runtime-0.3.0/collab/platform_probe.py +305 -0
  13. collab_runtime-0.3.0/collab/safe_subprocess.py +313 -0
  14. collab_runtime-0.3.0/collab/subprocess_bridge.py +26 -0
  15. {collab_runtime-0.2.9 → collab_runtime-0.3.0/collab_runtime.egg-info}/PKG-INFO +4 -4
  16. collab_runtime-0.3.0/collab_runtime.egg-info/SOURCES.txt +22 -0
  17. {collab_runtime-0.2.9 → collab_runtime-0.3.0}/collab_runtime.egg-info/entry_points.txt +1 -1
  18. collab_runtime-0.3.0/collab_runtime.egg-info/top_level.txt +1 -0
  19. collab_runtime-0.2.9/README_pypi.md → collab_runtime-0.3.0/docs/pypi/README.md +3 -3
  20. {collab_runtime-0.2.9 → collab_runtime-0.3.0}/pyproject.toml +9 -12
  21. collab_runtime-0.2.9/collab/__init__.py +0 -77
  22. collab_runtime-0.2.9/collab/__main__.py +0 -11
  23. collab_runtime-0.2.9/collab_runtime.egg-info/SOURCES.txt +0 -86
  24. collab_runtime-0.2.9/collab_runtime.egg-info/top_level.txt +0 -10
  25. collab_runtime-0.2.9/scripts/cleanup.py +0 -395
  26. collab_runtime-0.2.9/scripts/collab_git_hook.py +0 -190
  27. collab_runtime-0.2.9/scripts/format_code.py +0 -594
  28. collab_runtime-0.2.9/scripts/generate_tests.py +0 -560
  29. collab_runtime-0.2.9/scripts/validate_code.py +0 -1397
  30. collab_runtime-0.2.9/src/__init__.py +0 -4
  31. collab_runtime-0.2.9/tests/backend/__init__.py +0 -0
  32. collab_runtime-0.2.9/tests/backend/functional/__init__.py +0 -0
  33. collab_runtime-0.2.9/tests/backend/functional/test_package_imports.py +0 -43
  34. collab_runtime-0.2.9/tests/backend/integration/__init__.py +0 -0
  35. collab_runtime-0.2.9/tests/backend/integration/test_cli_contract_parity.py +0 -220
  36. collab_runtime-0.2.9/tests/backend/performance/__init__.py +0 -0
  37. collab_runtime-0.2.9/tests/backend/reliability/__init__.py +0 -0
  38. collab_runtime-0.2.9/tests/backend/security/__init__.py +0 -0
  39. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/__init__.py +0 -5
  40. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/_helpers.py +0 -123
  41. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/conftest.py +0 -18
  42. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +0 -188
  43. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +0 -56
  44. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +0 -459
  45. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +0 -1925
  46. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +0 -187
  47. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +0 -320
  48. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +0 -67
  49. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +0 -155
  50. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +0 -684
  51. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +0 -173
  52. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +0 -71
  53. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +0 -516
  54. collab_runtime-0.2.9/tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +0 -296
  55. collab_runtime-0.2.9/tests/backend/unit/lock_client/__init__.py +0 -1
  56. collab_runtime-0.2.9/tests/backend/unit/lock_client/_helpers.py +0 -132
  57. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_acquire.py +0 -214
  58. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_active.py +0 -104
  59. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_api.py +0 -63
  60. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_cli.py +0 -682
  61. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_daemon.py +0 -3730
  62. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_dashboard.py +0 -438
  63. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_discover.py +0 -241
  64. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_force_release.py +0 -354
  65. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_helper_branches.py +0 -1890
  66. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_history.py +0 -301
  67. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_isolation.py +0 -316
  68. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_pid.py +0 -75
  69. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_reconcile.py +0 -464
  70. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_release.py +0 -77
  71. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_shutdown.py +0 -1110
  72. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_utils.py +0 -474
  73. collab_runtime-0.2.9/tests/backend/unit/lock_client/test_lock_client_watch.py +0 -866
  74. collab_runtime-0.2.9/tests/backend/unit/scripts/__init__.py +0 -1
  75. collab_runtime-0.2.9/tests/backend/unit/scripts/_helpers.py +0 -42
  76. collab_runtime-0.2.9/tests/backend/unit/scripts/test_cleanup.py +0 -285
  77. collab_runtime-0.2.9/tests/backend/unit/scripts/test_collab_git_hook.py +0 -280
  78. collab_runtime-0.2.9/tests/backend/unit/scripts/test_collab_git_hook_ported.py +0 -50
  79. collab_runtime-0.2.9/tests/backend/unit/scripts/test_format_code.py +0 -368
  80. collab_runtime-0.2.9/tests/backend/unit/scripts/test_format_code_ported.py +0 -177
  81. collab_runtime-0.2.9/tests/backend/unit/scripts/test_generate_tests.py +0 -305
  82. collab_runtime-0.2.9/tests/backend/unit/scripts/test_hook_templates.py +0 -357
  83. collab_runtime-0.2.9/tests/backend/unit/scripts/test_setup_hook_overlay.py +0 -95
  84. collab_runtime-0.2.9/tests/backend/unit/scripts/test_validate_code.py +0 -867
  85. collab_runtime-0.2.9/tests/backend/unit/scripts/test_validate_code_ported.py +0 -237
  86. collab_runtime-0.2.9/tests/backend/unit/test_entrypoints_main_run.py +0 -83
  87. collab_runtime-0.2.9/tests/backend/unit/test_logging_config.py +0 -529
  88. collab_runtime-0.2.9/tests/backend/unit/test_main_watch_pid_file.py +0 -278
  89. collab_runtime-0.2.9/tests/conftest.py +0 -167
  90. collab_runtime-0.2.9/tests/frontend/__init__.py +0 -0
  91. collab_runtime-0.2.9/tests/frontend/jest/__init__.py +0 -0
  92. collab_runtime-0.2.9/tests/frontend/playwright/__init__.py +0 -0
  93. collab_runtime-0.2.9/tests/packaging/test_smoke_install.py +0 -76
  94. {collab_runtime-0.2.9 → collab_runtime-0.3.0}/LICENSE +0 -0
  95. {collab_runtime-0.2.9 → collab_runtime-0.3.0}/collab_runtime.egg-info/dependency_links.txt +0 -0
  96. {collab_runtime-0.2.9 → collab_runtime-0.3.0}/collab_runtime.egg-info/requires.txt +0 -0
  97. {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.2.9
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-extension/collab-locks/` directory
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 src/main.py --reason "Testing locking system"
106
- collab status src/main.py
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 src/main.py
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-extension/collab-locks/`
205
+ 2. Select `editors/vscode/collab-locks/`
206
206
  3. Reload VS Code
207
207
 
208
208
  **CLI Install:**
209
- If you have the `collab` CLI installed, the extension is often provisioned automatically by `scripts/setup-dev.ps1`.
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
- ├── src/
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
- │ │ ├── performance/ # Performance tests
319
- │ │ └── reliability/ # Reliability tests
318
+ │ │ └── (performance/ and reliability/ removed — empty placeholders deleted for clean/optimized structure)
320
319
  │ └── frontend/
321
- ├── jest/ # Frontend unit placeholder
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
- ├── schema.sql # Supabase database schema
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"
@@ -0,0 +1,8 @@
1
+ """Entrypoint for ``python -m collab`` and the ``collab`` console script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .main import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -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" class="btn btn-sm btn-primary" type="button">Active Locks</button>
495
- <button id="nav-history" class="btn btn-sm btn-outline-primary" type="button">History Locks</button>
496
- <button id="sync-btn" class="btn btn-sm btn-outline-secondary" type="button">
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" class="page-view hidden" aria-label="locks view">
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" aria-label="dashboard stats">
515
- <article class="stat-card">
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" id="stat-releases">0</p>
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" id="stat-avg">0m</p>
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" class="page-view hidden" aria-label="history view">
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
- function formatDateLong(dt) {
669
- return dt.toLocaleDateString([], {
670
- year: "numeric",
671
- month: "long",
672
- day: "numeric"
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
- const h = window.location.hash.replace("#", "").toLowerCase();
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"