collab-runtime 0.2.9__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 (97) hide show
  1. {collab_runtime-0.2.9/collab_runtime.egg-info → collab_runtime-0.3.1}/PKG-INFO +4 -4
  2. {collab_runtime-0.2.9 → collab_runtime-0.3.1}/README.md +36 -16
  3. collab_runtime-0.3.1/collab/__init__.py +24 -0
  4. collab_runtime-0.3.1/collab/__main__.py +8 -0
  5. {collab_runtime-0.2.9/src → collab_runtime-0.3.1/collab}/dashboard/index.html +138 -98
  6. collab_runtime-0.3.1/collab/dashboard_server.py +242 -0
  7. collab_runtime-0.3.1/collab/errors.py +86 -0
  8. {collab_runtime-0.2.9/src → collab_runtime-0.3.1/collab}/live_locks_watcher.py +44 -190
  9. {collab_runtime-0.2.9/src → collab_runtime-0.3.1/collab}/lock_client.py +376 -542
  10. {collab_runtime-0.2.9/src → collab_runtime-0.3.1/collab}/logging_config.py +1 -1
  11. {collab_runtime-0.2.9/src → collab_runtime-0.3.1/collab}/main.py +13 -1
  12. collab_runtime-0.3.1/collab/platform_probe.py +305 -0
  13. collab_runtime-0.3.1/collab/safe_subprocess.py +313 -0
  14. collab_runtime-0.3.1/collab/subprocess_bridge.py +26 -0
  15. {collab_runtime-0.2.9 → collab_runtime-0.3.1/collab_runtime.egg-info}/PKG-INFO +4 -4
  16. collab_runtime-0.3.1/collab_runtime.egg-info/SOURCES.txt +22 -0
  17. {collab_runtime-0.2.9 → collab_runtime-0.3.1}/collab_runtime.egg-info/entry_points.txt +1 -1
  18. collab_runtime-0.3.1/collab_runtime.egg-info/top_level.txt +1 -0
  19. collab_runtime-0.2.9/README_pypi.md → collab_runtime-0.3.1/docs/pypi/README.md +3 -3
  20. {collab_runtime-0.2.9 → collab_runtime-0.3.1}/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.1}/LICENSE +0 -0
  95. {collab_runtime-0.2.9 → collab_runtime-0.3.1}/collab_runtime.egg-info/dependency_links.txt +0 -0
  96. {collab_runtime-0.2.9 → collab_runtime-0.3.1}/collab_runtime.egg-info/requires.txt +0 -0
  97. {collab_runtime-0.2.9 → 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.2.9
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
@@ -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,74 @@
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="project-info"
494
+ class="chip hidden text-muted small"
495
+ data-testid="project-info"></span>
496
+ <span id="user-info" class="chip hidden" data-testid="user-info"></span>
497
+ <button id="nav-locks"
498
+ class="btn btn-sm btn-primary"
499
+ type="button"
500
+ data-testid="nav-locks">Active Locks</button>
501
+ <button id="nav-history"
502
+ class="btn btn-sm btn-outline-primary"
503
+ type="button"
504
+ data-testid="nav-history">History Locks</button>
505
+ <button id="sync-btn"
506
+ class="btn btn-sm btn-outline-secondary"
507
+ type="button"
508
+ data-testid="sync-btn">
497
509
  <i class="fas fa-sync-alt me-1"></i>Sync
498
510
  </button>
499
511
  </div>
500
512
  </header>
501
513
  <main class="app-main">
502
- <section id="setup-view" class="setup-view hidden">
514
+ <section id="setup-view" class="setup-view hidden" data-testid="setup-view">
503
515
  <h4 class="mb-2">Connect To Supabase</h4>
504
516
  <p class="text-muted mb-3">Configure credentials in .env and restart the dashboard server.</p>
505
517
  <pre class="bg-light border rounded p-3 mb-0">SUPABASE_URL=https://your-project.supabase.co
506
518
  SUPABASE_ANON_KEY=your_anon_key</pre>
507
519
  </section>
508
- <section id="locks-page" class="page-view hidden" aria-label="locks view">
520
+ <section id="locks-page"
521
+ class="page-view hidden"
522
+ aria-label="locks view"
523
+ data-testid="locks-page">
509
524
  <div class="page-head">
510
525
  <h2 class="page-title">
511
526
  <i class="fas fa-lock"></i>Active Locks
512
527
  </h2>
513
528
  </div>
514
- <div class="stats-grid" aria-label="dashboard stats">
515
- <article class="stat-card">
529
+ <div class="stats-grid"
530
+ aria-label="dashboard stats"
531
+ data-testid="stats-grid">
532
+ <article class="stat-card" data-testid="stat-card-active">
516
533
  <div class="stat-shell">
517
534
  <span class="stat-icon stat-icon-lock"><i class="fas fa-lock"></i></span>
518
535
  <div>
519
536
  <p class="stat-label">Active Locks</p>
520
- <p class="stat-value" id="stat-active">0</p>
537
+ <p class="stat-value" id="stat-active" data-testid="stat-active">0</p>
521
538
  </div>
522
539
  </div>
523
540
  </article>
524
- <article class="stat-card">
541
+ <article class="stat-card" data-testid="stat-card-releases">
525
542
  <div class="stat-shell">
526
543
  <span class="stat-icon stat-icon-release"><i class="fas fa-check-double"></i></span>
527
544
  <div>
528
545
  <p class="stat-label">Releases Today</p>
529
- <p class="stat-value stat-value-success" id="stat-releases">0</p>
546
+ <p class="stat-value stat-value-success"
547
+ id="stat-releases"
548
+ data-testid="stat-releases">0</p>
530
549
  </div>
531
550
  </div>
532
551
  </article>
533
- <article class="stat-card">
552
+ <article class="stat-card" data-testid="stat-card-avg">
534
553
  <div class="stat-shell">
535
554
  <span class="stat-icon stat-icon-avg"><i class="fas fa-bolt"></i></span>
536
555
  <div>
537
556
  <p class="stat-label">Avg Hold Time</p>
538
- <p class="stat-value stat-value-info" id="stat-avg">0m</p>
557
+ <p class="stat-value stat-value-info"
558
+ id="stat-avg"
559
+ data-testid="stat-avg">0m</p>
539
560
  </div>
540
561
  </div>
541
562
  </article>
@@ -553,7 +574,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
553
574
  <th class="text-end">Action</th>
554
575
  </tr>
555
576
  </thead>
556
- <tbody id="active-locks-body">
577
+ <tbody id="active-locks-body" data-testid="active-locks-body">
557
578
  <tr>
558
579
  <td colspan="6" class="text-center py-4 text-muted">Connecting...</td>
559
580
  </tr>
@@ -562,7 +583,10 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
562
583
  </div>
563
584
  </div>
564
585
  </section>
565
- <section id="history-page" class="page-view hidden" aria-label="history view">
586
+ <section id="history-page"
587
+ class="page-view hidden"
588
+ aria-label="history view"
589
+ data-testid="history-page">
566
590
  <div class="page-head">
567
591
  <h2 class="page-title">
568
592
  <i class="fas fa-clock-rotate-left"></i>Lock History
@@ -583,7 +607,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
583
607
  <th>Outcome</th>
584
608
  </tr>
585
609
  </thead>
586
- <tbody id="history-body">
610
+ <tbody id="history-body" data-testid="history-body">
587
611
  <tr>
588
612
  <td colspan="8" class="text-center py-4 text-muted">Loading history...</td>
589
613
  </tr>
@@ -626,13 +650,16 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
626
650
  </div>
627
651
  </div>
628
652
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
653
+ <script src="dashboard-format.js"></script>
629
654
  <script>
630
- const serverCfg = window.__SUPABASE_CONFIG__ || {};
631
- const SUPABASE_URL = serverCfg.url || "";
632
- const SUPABASE_KEY = serverCfg.serviceKey || serverCfg.anonKey || "";
633
- const SUPABASE_USER = serverCfg.user || null;
634
- const IS_ADMIN = !!serverCfg.serviceKey;
635
- 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";
636
663
 
637
664
  const PAGE_SIZE = 25;
638
665
  const HISTORY_PREFETCH_GAP_PX = 120;
@@ -665,56 +692,14 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
665
692
  }
666
693
  }
667
694
 
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
- }
695
+ const fmt = window.DashboardFormat;
696
+ const formatDateLong = fmt.formatDateLong;
697
+ const formatTime24 = fmt.formatTime24;
698
+ const formatDateTime24 = fmt.formatDateTime24;
699
+ const formatDurationMinutes = fmt.formatDurationMinutes;
714
700
 
715
701
  function routeFromHash() {
716
- const h = window.location.hash.replace("#", "").toLowerCase();
717
- return h === "history" ? "history" : "locks";
702
+ return fmt.routeFromHash(window.location.hash);
718
703
  }
719
704
 
720
705
  function navigate(page) {
@@ -764,14 +749,57 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
764
749
  document.getElementById("nav-history").disabled = true;
765
750
  }
766
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
+
767
793
  function showMain() {
768
794
  document.getElementById("setup-view").classList.add("hidden");
769
795
  document.getElementById("locks-page").classList.remove("hidden");
770
796
  document.getElementById("history-page").classList.remove("hidden");
797
+ const userInfo = document.getElementById("user-info");
771
798
  if (SUPABASE_USER) {
772
- const userInfo = document.getElementById("user-info");
773
799
  userInfo.classList.remove("hidden");
774
800
  userInfo.innerHTML = '<i class="fab fa-github me-1"></i>' + SUPABASE_USER;
801
+ } else {
802
+ userInfo.classList.add("hidden");
775
803
  }
776
804
  }
777
805
 
@@ -783,33 +811,40 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
783
811
  '</td></tr>';
784
812
  }
785
813
 
786
- function loadSupabaseClient() {
814
+ function loadSupabaseLibrary() {
787
815
  return new Promise((resolve, reject) => {
788
816
  if (window.supabase) {
789
- try {
790
- supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
791
- resolve();
792
- } catch (e) {
793
- reject(e);
794
- }
817
+ resolve();
795
818
  return;
796
819
  }
797
-
798
820
  const script = document.createElement("script");
799
821
  script.src = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js/dist/umd/supabase.min.js";
800
- script.onload = () => {
801
- try {
802
- supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
803
- resolve();
804
- } catch (e) {
805
- reject(e);
806
- }
807
- };
822
+ script.onload = () => resolve();
808
823
  script.onerror = reject;
809
824
  document.head.appendChild(script);
810
825
  });
811
826
  }
812
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
+
813
848
  function subscribeRealtime() {
814
849
  if (!supabaseClient) {
815
850
  return;
@@ -840,6 +875,10 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
840
875
  const syncBtn = document.getElementById("sync-btn");
841
876
  syncBtn.disabled = true;
842
877
  try {
878
+ if (!(await ensureSupabaseClient())) {
879
+ showSetup();
880
+ return;
881
+ }
843
882
  const { data, error } = await supabaseClient
844
883
  .from("file_locks")
845
884
  .select("*")
@@ -864,7 +903,7 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
864
903
  console.error(e);
865
904
  showLocksError("Unable to fetch active locks.");
866
905
  } finally {
867
- syncBtn.disabled = false;
906
+ syncBtn.disabled = !SUPABASE_MODE;
868
907
  }
869
908
  }
870
909
 
@@ -949,6 +988,9 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
949
988
 
950
989
  historyLoading = true;
951
990
  try {
991
+ if (!(await ensureSupabaseClient())) {
992
+ return;
993
+ }
952
994
  const from = historyOffset;
953
995
  const to = historyOffset + PAGE_SIZE - 1;
954
996
  const { data, error } = await supabaseClient
@@ -1102,17 +1144,15 @@ SUPABASE_ANON_KEY=your_anon_key</pre>
1102
1144
  async function init() {
1103
1145
  releaseModal = new bootstrap.Modal(document.getElementById("releaseModal"));
1104
1146
  wireEvents();
1105
-
1106
- if (!SUPABASE_MODE) {
1107
- showSetup();
1108
- return;
1109
- }
1110
-
1111
- showMain();
1112
- applyRoute();
1147
+ applyRuntimeConfig();
1113
1148
 
1114
1149
  try {
1115
- await loadSupabaseClient();
1150
+ if (!(await ensureSupabaseClient())) {
1151
+ showSetup();
1152
+ return;
1153
+ }
1154
+ showMain();
1155
+ applyRoute();
1116
1156
  await refreshLocks();
1117
1157
  if (routeFromHash() === "history") {
1118
1158
  resetHistory();