collab-runtime 0.2.9__py3-none-any.whl

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 (82) hide show
  1. collab/__init__.py +77 -0
  2. collab/__main__.py +11 -0
  3. collab_runtime-0.2.9.dist-info/METADATA +218 -0
  4. collab_runtime-0.2.9.dist-info/RECORD +82 -0
  5. collab_runtime-0.2.9.dist-info/WHEEL +5 -0
  6. collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
  7. collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
  8. collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
  9. scripts/cleanup.py +395 -0
  10. scripts/collab_git_hook.py +190 -0
  11. scripts/format_code.py +594 -0
  12. scripts/generate_tests.py +560 -0
  13. scripts/validate_code.py +1397 -0
  14. src/__init__.py +4 -0
  15. src/dashboard/index.html +1131 -0
  16. src/live_locks_watcher.py +1982 -0
  17. src/lock_client.py +4268 -0
  18. src/logging_config.py +259 -0
  19. src/main.py +436 -0
  20. tests/backend/__init__.py +0 -0
  21. tests/backend/functional/__init__.py +0 -0
  22. tests/backend/functional/test_package_imports.py +43 -0
  23. tests/backend/integration/__init__.py +0 -0
  24. tests/backend/integration/test_cli_contract_parity.py +220 -0
  25. tests/backend/performance/__init__.py +0 -0
  26. tests/backend/reliability/__init__.py +0 -0
  27. tests/backend/security/__init__.py +0 -0
  28. tests/backend/unit/live_locks_watcher/__init__.py +5 -0
  29. tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
  30. tests/backend/unit/live_locks_watcher/conftest.py +18 -0
  31. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
  32. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
  33. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
  34. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
  35. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
  36. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
  37. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
  38. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
  39. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
  40. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
  41. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
  42. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
  43. tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
  44. tests/backend/unit/lock_client/__init__.py +1 -0
  45. tests/backend/unit/lock_client/_helpers.py +132 -0
  46. tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
  47. tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
  48. tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
  49. tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
  50. tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
  51. tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
  52. tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
  53. tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
  54. tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
  55. tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
  56. tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
  57. tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
  58. tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
  59. tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
  60. tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
  61. tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
  62. tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
  63. tests/backend/unit/scripts/__init__.py +1 -0
  64. tests/backend/unit/scripts/_helpers.py +42 -0
  65. tests/backend/unit/scripts/test_cleanup.py +285 -0
  66. tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
  67. tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
  68. tests/backend/unit/scripts/test_format_code.py +368 -0
  69. tests/backend/unit/scripts/test_format_code_ported.py +177 -0
  70. tests/backend/unit/scripts/test_generate_tests.py +305 -0
  71. tests/backend/unit/scripts/test_hook_templates.py +357 -0
  72. tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
  73. tests/backend/unit/scripts/test_validate_code.py +867 -0
  74. tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
  75. tests/backend/unit/test_entrypoints_main_run.py +83 -0
  76. tests/backend/unit/test_logging_config.py +529 -0
  77. tests/backend/unit/test_main_watch_pid_file.py +278 -0
  78. tests/conftest.py +167 -0
  79. tests/frontend/__init__.py +0 -0
  80. tests/frontend/jest/__init__.py +0 -0
  81. tests/frontend/playwright/__init__.py +0 -0
  82. tests/packaging/test_smoke_install.py +76 -0
@@ -0,0 +1,1131 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Collaborative Lock Dashboard</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
8
+ rel="stylesheet">
9
+ <link rel="stylesheet"
10
+ href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
12
+ rel="stylesheet">
13
+ <style>
14
+ :root {
15
+ --bg-body: #f5f7fb;
16
+ --bg-card: #ffffff;
17
+ --text-main: #1e293b;
18
+ --text-muted: #64748b;
19
+ --border-color: #e2e8f0;
20
+ --primary: #4f46e5;
21
+ --primary-soft: #eef2ff;
22
+ --danger: #dc2626;
23
+ --nav-h: 72px;
24
+ --radius: 12px;
25
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
26
+ --shadow-md: 0 10px 22px -14px rgb(30 41 59 / 0.35);
27
+ --shadow-lg: 0 26px 48px -28px rgb(30 41 59 / 0.45);
28
+ }
29
+
30
+ * {
31
+ box-sizing: border-box;
32
+ }
33
+
34
+ html,
35
+ body {
36
+ width: 100%;
37
+ height: 100%;
38
+ margin: 0;
39
+ overflow: hidden;
40
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
41
+ background: var(--bg-body);
42
+ color: var(--text-main);
43
+ }
44
+
45
+ body {
46
+ display: flex;
47
+ flex-direction: column;
48
+ -webkit-font-smoothing: antialiased;
49
+ }
50
+
51
+ .app-nav {
52
+ height: var(--nav-h);
53
+ min-height: var(--nav-h);
54
+ padding: 0 1.15rem;
55
+ border-bottom: 1px solid var(--border-color);
56
+ background: rgba(255, 255, 255, 0.84);
57
+ backdrop-filter: blur(14px);
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ gap: 1rem;
62
+ }
63
+
64
+ .brand {
65
+ font-weight: 800;
66
+ font-size: 1.1rem;
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 0.6rem;
70
+ }
71
+
72
+ .brand i {
73
+ color: var(--primary);
74
+ }
75
+
76
+ .brand span {
77
+ background: linear-gradient(135deg, var(--primary), #818cf8);
78
+ -webkit-background-clip: text;
79
+ background-clip: text;
80
+ -webkit-text-fill-color: transparent;
81
+ }
82
+
83
+ .nav-right {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: 0.6rem;
87
+ }
88
+
89
+ .nav-right .btn {
90
+ border-radius: 999px;
91
+ font-weight: 700;
92
+ box-shadow: var(--shadow-sm);
93
+ transition: all 0.2s ease;
94
+ }
95
+
96
+ .nav-right .btn.btn-primary {
97
+ border-color: var(--primary);
98
+ background: linear-gradient(135deg, var(--primary), #6366f1);
99
+ }
100
+
101
+ .nav-right .btn:hover {
102
+ transform: translateY(-1px);
103
+ box-shadow: var(--shadow-md);
104
+ }
105
+
106
+ .nav-right .btn.btn-outline-primary {
107
+ border-color: #c7d2fe;
108
+ color: #4338ca;
109
+ background: #fff;
110
+ }
111
+
112
+ .nav-right .btn.btn-outline-secondary {
113
+ border-color: var(--border-color);
114
+ color: #475569;
115
+ background: #f8fafc;
116
+ }
117
+
118
+ .chip {
119
+ border: 1px solid var(--border-color);
120
+ border-radius: 999px;
121
+ padding: 0.4rem 0.8rem;
122
+ font-size: 0.82rem;
123
+ font-weight: 600;
124
+ background: #fff;
125
+ color: var(--text-muted);
126
+ box-shadow: var(--shadow-sm);
127
+ }
128
+
129
+ .app-main {
130
+ height: calc(100vh - var(--nav-h));
131
+ min-height: 0;
132
+ padding: 1rem 1.15rem;
133
+ }
134
+
135
+ .page-view {
136
+ width: 100%;
137
+ height: 100%;
138
+ min-height: 0;
139
+ display: none;
140
+ flex-direction: column;
141
+ gap: 0.95rem;
142
+ }
143
+
144
+ .page-view.active {
145
+ display: flex;
146
+ }
147
+
148
+ .page-head {
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: space-between;
152
+ gap: 0.75rem;
153
+ flex-wrap: wrap;
154
+ }
155
+
156
+ .page-title {
157
+ margin: 0;
158
+ font-size: 1rem;
159
+ font-weight: 800;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 0.5rem;
163
+ }
164
+
165
+ .table-shell {
166
+ background: var(--bg-card);
167
+ border: 1px solid var(--border-color);
168
+ border-radius: var(--radius);
169
+ min-height: 0;
170
+ height: 100%;
171
+ display: flex;
172
+ flex-direction: column;
173
+ overflow: hidden;
174
+ box-shadow: var(--shadow-sm);
175
+ transition: box-shadow 0.2s ease;
176
+ }
177
+
178
+ .table-shell:hover {
179
+ box-shadow: var(--shadow-md);
180
+ }
181
+
182
+ .table-scroll {
183
+ min-height: 0;
184
+ flex: 1;
185
+ overflow: auto;
186
+ }
187
+
188
+ .table {
189
+ margin: 0;
190
+ }
191
+
192
+ .table thead th {
193
+ position: sticky;
194
+ top: 0;
195
+ z-index: 5;
196
+ background: #f8fafc;
197
+ border-bottom: 1px solid var(--border-color);
198
+ color: var(--text-muted);
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.04em;
201
+ font-size: 0.72rem;
202
+ font-weight: 700;
203
+ white-space: nowrap;
204
+ padding: 0.88rem 0.75rem;
205
+ }
206
+
207
+ .table tbody td {
208
+ font-size: 0.84rem;
209
+ padding: 0.9rem 0.75rem;
210
+ vertical-align: middle;
211
+ border-color: var(--border-color);
212
+ }
213
+
214
+ .table tbody tr:hover {
215
+ background: #f8fafc;
216
+ }
217
+
218
+ .stats-grid {
219
+ display: grid;
220
+ grid-template-columns: repeat(3, minmax(0, 1fr));
221
+ gap: 1.1rem;
222
+ }
223
+
224
+ .stat-card {
225
+ background: var(--bg-card);
226
+ border: 1px solid var(--border-color);
227
+ border-radius: var(--radius);
228
+ padding: 1.35rem;
229
+ box-shadow: var(--shadow-sm);
230
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
231
+ }
232
+
233
+ .stat-card:hover {
234
+ transform: translateY(-2px);
235
+ box-shadow: var(--shadow-md);
236
+ }
237
+
238
+ .stat-shell {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 0.9rem;
242
+ }
243
+
244
+ .stat-icon {
245
+ width: 44px;
246
+ height: 44px;
247
+ border-radius: 10px;
248
+ display: inline-flex;
249
+ align-items: center;
250
+ justify-content: center;
251
+ font-size: 1.1rem;
252
+ }
253
+
254
+ .stat-icon-lock {
255
+ background: #dbeafe;
256
+ color: #2563eb;
257
+ }
258
+
259
+ .stat-icon-release {
260
+ background: #dcfce7;
261
+ color: #16a34a;
262
+ }
263
+
264
+ .stat-icon-avg {
265
+ background: #e0f2fe;
266
+ color: #0891b2;
267
+ }
268
+
269
+ .stat-label {
270
+ margin: 0;
271
+ font-size: 0.74rem;
272
+ font-weight: 700;
273
+ text-transform: uppercase;
274
+ letter-spacing: 0.06em;
275
+ color: var(--text-muted);
276
+ }
277
+
278
+ .stat-value {
279
+ margin: 0.15rem 0 0;
280
+ font-size: 2.08rem;
281
+ line-height: 1;
282
+ font-weight: 800;
283
+ color: #0f172a;
284
+ }
285
+
286
+ .stat-value-success {
287
+ color: #16a34a;
288
+ }
289
+
290
+ .stat-value-info {
291
+ color: #0891b2;
292
+ }
293
+
294
+ .dev-tag {
295
+ display: inline-flex;
296
+ align-items: center;
297
+ padding: 0.18rem 0.5rem;
298
+ border-radius: 0.35rem;
299
+ border: 1px solid var(--border-color);
300
+ background: #fff;
301
+ font-size: 0.75rem;
302
+ font-weight: 700;
303
+ }
304
+
305
+ .dev-tag-owner {
306
+ border-color: #bfdbfe;
307
+ color: #1d4ed8;
308
+ background: var(--primary-soft);
309
+ }
310
+
311
+ code {
312
+ color: #1d4ed8;
313
+ background: #f8fafc;
314
+ border: 1px solid var(--border-color);
315
+ border-radius: 0.35rem;
316
+ padding: 0.12rem 0.4rem;
317
+ font-size: 0.78rem;
318
+ }
319
+
320
+ .table-shell .btn-outline-danger {
321
+ border-color: #fca5a5;
322
+ color: #dc2626;
323
+ background: #fff;
324
+ font-weight: 700;
325
+ border-radius: 999px;
326
+ padding: 0.3rem 0.82rem;
327
+ }
328
+
329
+ .table-shell .btn-outline-danger:hover {
330
+ border-color: #ef4444;
331
+ background: #ef4444;
332
+ color: #fff;
333
+ }
334
+
335
+ .setup-view {
336
+ max-width: 640px;
337
+ margin: 2rem auto;
338
+ background: #fff;
339
+ border: 1px solid var(--border-color);
340
+ border-top: 4px solid var(--primary);
341
+ border-radius: 0.75rem;
342
+ padding: 1.5rem;
343
+ box-shadow: var(--shadow-sm);
344
+ }
345
+
346
+ .hidden {
347
+ display: none !important;
348
+ }
349
+
350
+ .release-modal .modal-content {
351
+ border-radius: 16px;
352
+ border: 1px solid #e5e7eb;
353
+ box-shadow: var(--shadow-lg);
354
+ }
355
+
356
+ .release-modal .modal-header,
357
+ .release-modal .modal-footer {
358
+ border: 0;
359
+ }
360
+
361
+ .release-modal .modal-title {
362
+ color: #dc2626;
363
+ font-weight: 800;
364
+ font-size: 1.95rem;
365
+ }
366
+
367
+ .release-modal .modal-body {
368
+ padding-top: 0.5rem;
369
+ padding-bottom: 0.45rem;
370
+ }
371
+
372
+ .release-help {
373
+ margin-bottom: 1rem;
374
+ color: #4b5563;
375
+ font-size: 1rem;
376
+ line-height: 1.45;
377
+ }
378
+
379
+ .release-file-box {
380
+ border-radius: 14px;
381
+ border: 1px solid #d1d5db;
382
+ background: #f9fafb;
383
+ padding: 0.85rem 1rem;
384
+ margin-bottom: 1rem;
385
+ }
386
+
387
+ .release-file-label {
388
+ margin: 0;
389
+ font-size: 0.76rem;
390
+ font-weight: 700;
391
+ color: #6b7280;
392
+ text-transform: uppercase;
393
+ letter-spacing: 0.04em;
394
+ }
395
+
396
+ .release-file-path {
397
+ margin-top: 0.35rem;
398
+ font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
399
+ font-size: 1.02rem;
400
+ color: #111827;
401
+ word-break: break-word;
402
+ }
403
+
404
+ .release-warning {
405
+ display: flex;
406
+ align-items: flex-start;
407
+ gap: 0.7rem;
408
+ border-radius: 14px;
409
+ background: #fef3c7;
410
+ color: #92400e;
411
+ padding: 0.85rem 1rem;
412
+ margin-bottom: 0.25rem;
413
+ }
414
+
415
+ .release-warning i {
416
+ color: #f59e0b;
417
+ margin-top: 0.12rem;
418
+ }
419
+
420
+ .release-warning-text {
421
+ margin: 0;
422
+ font-size: 0.98rem;
423
+ line-height: 1.35;
424
+ font-weight: 600;
425
+ }
426
+
427
+ .release-modal .btn {
428
+ min-width: 170px;
429
+ font-size: 1.02rem;
430
+ font-weight: 700;
431
+ border-radius: 12px;
432
+ padding: 0.72rem 1.25rem;
433
+ transition: all 0.2s ease;
434
+ }
435
+
436
+ .release-modal .btn-cancel {
437
+ background: #e5e7eb;
438
+ border-color: #e5e7eb;
439
+ color: #475569;
440
+ }
441
+
442
+ .release-modal .btn-cancel:hover {
443
+ background: #d1d5db;
444
+ border-color: #d1d5db;
445
+ color: #334155;
446
+ }
447
+
448
+ .release-modal .btn-confirm {
449
+ background: linear-gradient(90deg, #ef4444, #f87171);
450
+ border-color: #ef4444;
451
+ color: #fff;
452
+ }
453
+
454
+ .release-modal .btn-confirm:hover {
455
+ background: linear-gradient(90deg, #dc2626, #ef4444);
456
+ border-color: #dc2626;
457
+ color: #fff;
458
+ transform: translateY(-1px);
459
+ }
460
+
461
+ @media (max-width: 768px) {
462
+ .app-nav {
463
+ padding: 0 0.75rem;
464
+ }
465
+
466
+ .app-main {
467
+ padding: 0.75rem;
468
+ }
469
+
470
+ .stats-grid {
471
+ grid-template-columns: 1fr;
472
+ }
473
+
474
+ .stat-value {
475
+ font-size: 1.7rem;
476
+ }
477
+
478
+ .table thead th,
479
+ .table tbody td {
480
+ padding: 0.62rem;
481
+ }
482
+ }
483
+ </style>
484
+ </head>
485
+ <body>
486
+ <header class="app-nav">
487
+ <div class="brand">
488
+ <i class="fas fa-shield-halved"></i>
489
+ <span>Collaborative Explorer</span>
490
+ </div>
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">
497
+ <i class="fas fa-sync-alt me-1"></i>Sync
498
+ </button>
499
+ </div>
500
+ </header>
501
+ <main class="app-main">
502
+ <section id="setup-view" class="setup-view hidden">
503
+ <h4 class="mb-2">Connect To Supabase</h4>
504
+ <p class="text-muted mb-3">Configure credentials in .env and restart the dashboard server.</p>
505
+ <pre class="bg-light border rounded p-3 mb-0">SUPABASE_URL=https://your-project.supabase.co
506
+ SUPABASE_ANON_KEY=your_anon_key</pre>
507
+ </section>
508
+ <section id="locks-page" class="page-view hidden" aria-label="locks view">
509
+ <div class="page-head">
510
+ <h2 class="page-title">
511
+ <i class="fas fa-lock"></i>Active Locks
512
+ </h2>
513
+ </div>
514
+ <div class="stats-grid" aria-label="dashboard stats">
515
+ <article class="stat-card">
516
+ <div class="stat-shell">
517
+ <span class="stat-icon stat-icon-lock"><i class="fas fa-lock"></i></span>
518
+ <div>
519
+ <p class="stat-label">Active Locks</p>
520
+ <p class="stat-value" id="stat-active">0</p>
521
+ </div>
522
+ </div>
523
+ </article>
524
+ <article class="stat-card">
525
+ <div class="stat-shell">
526
+ <span class="stat-icon stat-icon-release"><i class="fas fa-check-double"></i></span>
527
+ <div>
528
+ <p class="stat-label">Releases Today</p>
529
+ <p class="stat-value stat-value-success" id="stat-releases">0</p>
530
+ </div>
531
+ </div>
532
+ </article>
533
+ <article class="stat-card">
534
+ <div class="stat-shell">
535
+ <span class="stat-icon stat-icon-avg"><i class="fas fa-bolt"></i></span>
536
+ <div>
537
+ <p class="stat-label">Avg Hold Time</p>
538
+ <p class="stat-value stat-value-info" id="stat-avg">0m</p>
539
+ </div>
540
+ </div>
541
+ </article>
542
+ </div>
543
+ <div class="table-shell">
544
+ <div class="table-scroll" id="locks-scroll">
545
+ <table class="table table-sm align-middle">
546
+ <thead>
547
+ <tr>
548
+ <th>File Path</th>
549
+ <th>Developer</th>
550
+ <th>Branch</th>
551
+ <th>Reason</th>
552
+ <th>Timeline</th>
553
+ <th class="text-end">Action</th>
554
+ </tr>
555
+ </thead>
556
+ <tbody id="active-locks-body">
557
+ <tr>
558
+ <td colspan="6" class="text-center py-4 text-muted">Connecting...</td>
559
+ </tr>
560
+ </tbody>
561
+ </table>
562
+ </div>
563
+ </div>
564
+ </section>
565
+ <section id="history-page" class="page-view hidden" aria-label="history view">
566
+ <div class="page-head">
567
+ <h2 class="page-title">
568
+ <i class="fas fa-clock-rotate-left"></i>Lock History
569
+ </h2>
570
+ </div>
571
+ <div class="table-shell">
572
+ <div class="table-scroll" id="history-scroll">
573
+ <table class="table table-sm align-middle">
574
+ <thead>
575
+ <tr>
576
+ <th>File Path</th>
577
+ <th>Developer</th>
578
+ <th>Branch</th>
579
+ <th>Reason</th>
580
+ <th>Acquired At</th>
581
+ <th>Released At</th>
582
+ <th>Duration</th>
583
+ <th>Outcome</th>
584
+ </tr>
585
+ </thead>
586
+ <tbody id="history-body">
587
+ <tr>
588
+ <td colspan="8" class="text-center py-4 text-muted">Loading history...</td>
589
+ </tr>
590
+ </tbody>
591
+ </table>
592
+ </div>
593
+ </div>
594
+ </section>
595
+ </main>
596
+ <div class="modal fade release-modal"
597
+ id="releaseModal"
598
+ tabindex="-1"
599
+ aria-hidden="true">
600
+ <div class="modal-dialog modal-dialog-centered">
601
+ <div class="modal-content">
602
+ <div class="modal-header">
603
+ <h5 class="modal-title" id="modal-title">Release File Lock</h5>
604
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
605
+ </div>
606
+ <div class="modal-body">
607
+ <p id="modal-description" class="release-help">
608
+ Proceeding will release the lock for this file, allowing other team members to acquire it immediately.
609
+ </p>
610
+ <div class="release-file-box">
611
+ <p class="release-file-label">File Path</p>
612
+ <div id="modal-file-path" class="release-file-path"></div>
613
+ </div>
614
+ <div class="release-warning">
615
+ <i class="fas fa-exclamation-circle"></i>
616
+ <p id="modal-warning" class="release-warning-text">
617
+ Warning: Verify that you have committed your changes to avoid potential conflicts with other developers.
618
+ </p>
619
+ </div>
620
+ </div>
621
+ <div class="modal-footer">
622
+ <button type="button" class="btn btn-cancel" data-bs-dismiss="modal">Cancel</button>
623
+ <button type="button" class="btn btn-confirm" id="confirm-release-btn">Confirm Release</button>
624
+ </div>
625
+ </div>
626
+ </div>
627
+ </div>
628
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
629
+ <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);
636
+
637
+ const PAGE_SIZE = 25;
638
+ const HISTORY_PREFETCH_GAP_PX = 120;
639
+
640
+ let supabaseClient = null;
641
+ let releaseModal = null;
642
+ let pendingReleasePath = null;
643
+
644
+ let historyOffset = 0;
645
+ let historyHasMore = true;
646
+ let historyLoading = false;
647
+
648
+ function setNavActive(page) {
649
+ const locksBtn = document.getElementById("nav-locks");
650
+ const historyBtn = document.getElementById("nav-history");
651
+ if (page === "history") {
652
+ locksBtn.classList.remove("btn-primary");
653
+ locksBtn.classList.add("btn-outline-primary");
654
+ locksBtn.disabled = false;
655
+ historyBtn.classList.add("btn-primary");
656
+ historyBtn.classList.remove("btn-outline-primary");
657
+ historyBtn.disabled = true;
658
+ } else {
659
+ historyBtn.classList.remove("btn-primary");
660
+ historyBtn.classList.add("btn-outline-primary");
661
+ historyBtn.disabled = false;
662
+ locksBtn.classList.add("btn-primary");
663
+ locksBtn.classList.remove("btn-outline-primary");
664
+ locksBtn.disabled = true;
665
+ }
666
+ }
667
+
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
+ }
714
+
715
+ function routeFromHash() {
716
+ const h = window.location.hash.replace("#", "").toLowerCase();
717
+ return h === "history" ? "history" : "locks";
718
+ }
719
+
720
+ function navigate(page) {
721
+ const target = page === "history" ? "history" : "locks";
722
+ if (window.location.hash !== "#" + target) {
723
+ window.location.hash = target;
724
+ return;
725
+ }
726
+ applyRoute();
727
+ }
728
+
729
+ function applyRoute() {
730
+ const page = routeFromHash();
731
+ const locksPage = document.getElementById("locks-page");
732
+ const historyPage = document.getElementById("history-page");
733
+
734
+ if (page === "history") {
735
+ locksPage.classList.remove("active");
736
+ historyPage.classList.add("active");
737
+ setNavActive("history");
738
+ if (historyOffset === 0 && !historyLoading) {
739
+ resetHistory();
740
+ }
741
+ } else {
742
+ historyPage.classList.remove("active");
743
+ locksPage.classList.add("active");
744
+ setNavActive("locks");
745
+ }
746
+ }
747
+
748
+ function updateTimestamp() {
749
+ const stamp = new Date().toLocaleTimeString([], {
750
+ hour: "2-digit",
751
+ minute: "2-digit",
752
+ second: "2-digit",
753
+ hour12: false
754
+ });
755
+ document.getElementById("last-update").textContent = "Synced " + stamp;
756
+ }
757
+
758
+ function showSetup() {
759
+ document.getElementById("setup-view").classList.remove("hidden");
760
+ document.getElementById("locks-page").classList.add("hidden");
761
+ document.getElementById("history-page").classList.add("hidden");
762
+ document.getElementById("sync-btn").disabled = true;
763
+ document.getElementById("nav-locks").disabled = true;
764
+ document.getElementById("nav-history").disabled = true;
765
+ }
766
+
767
+ function showMain() {
768
+ document.getElementById("setup-view").classList.add("hidden");
769
+ document.getElementById("locks-page").classList.remove("hidden");
770
+ document.getElementById("history-page").classList.remove("hidden");
771
+ if (SUPABASE_USER) {
772
+ const userInfo = document.getElementById("user-info");
773
+ userInfo.classList.remove("hidden");
774
+ userInfo.innerHTML = '<i class="fab fa-github me-1"></i>' + SUPABASE_USER;
775
+ }
776
+ }
777
+
778
+ function showLocksError(msg) {
779
+ const tbody = document.getElementById("active-locks-body");
780
+ tbody.innerHTML =
781
+ '<tr><td colspan="6" class="text-danger text-center py-4">' +
782
+ msg +
783
+ '</td></tr>';
784
+ }
785
+
786
+ function loadSupabaseClient() {
787
+ return new Promise((resolve, reject) => {
788
+ if (window.supabase) {
789
+ try {
790
+ supabaseClient = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY);
791
+ resolve();
792
+ } catch (e) {
793
+ reject(e);
794
+ }
795
+ return;
796
+ }
797
+
798
+ const script = document.createElement("script");
799
+ 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
+ };
808
+ script.onerror = reject;
809
+ document.head.appendChild(script);
810
+ });
811
+ }
812
+
813
+ function subscribeRealtime() {
814
+ if (!supabaseClient) {
815
+ return;
816
+ }
817
+ try {
818
+ const ch = supabaseClient.channel("dashboard-realtime");
819
+ ch.on(
820
+ "postgres_changes",
821
+ { event: "*", schema: "public", table: "file_locks" },
822
+ () => refreshLocks()
823
+ );
824
+ ch.on(
825
+ "postgres_changes",
826
+ { event: "*", schema: "public", table: "file_locks_history" },
827
+ () => {
828
+ if (routeFromHash() === "history") {
829
+ resetHistory();
830
+ }
831
+ }
832
+ );
833
+ ch.subscribe();
834
+ } catch (e) {
835
+ console.warn("Realtime subscription failed", e);
836
+ }
837
+ }
838
+
839
+ async function refreshLocks() {
840
+ const syncBtn = document.getElementById("sync-btn");
841
+ syncBtn.disabled = true;
842
+ try {
843
+ const { data, error } = await supabaseClient
844
+ .from("file_locks")
845
+ .select("*")
846
+ .neq("is_ephemeral", true)
847
+ .order("acquired_at", { ascending: false });
848
+ if (error) {
849
+ throw error;
850
+ }
851
+ const { data: historyData, error: historyError } = await supabaseClient
852
+ .from("file_locks_history")
853
+ .select("acquired_at,released_at")
854
+ .neq("is_ephemeral", true)
855
+ .order("id", { ascending: false })
856
+ .limit(250);
857
+ if (historyError) {
858
+ throw historyError;
859
+ }
860
+ renderActiveLocks(data || []);
861
+ updateStats(data || [], historyData || []);
862
+ updateTimestamp();
863
+ } catch (e) {
864
+ console.error(e);
865
+ showLocksError("Unable to fetch active locks.");
866
+ } finally {
867
+ syncBtn.disabled = false;
868
+ }
869
+ }
870
+
871
+ function updateStats(locks, history) {
872
+ document.getElementById("stat-active").textContent = String(locks.length);
873
+ const today = new Date().toDateString();
874
+ const todayReleases = history.filter((h) => {
875
+ if (!h.released_at) {
876
+ return false;
877
+ }
878
+ return new Date(h.released_at).toDateString() === today;
879
+ }).length;
880
+ document.getElementById("stat-releases").textContent = String(todayReleases);
881
+
882
+ const durations = history
883
+ .filter((h) => h.acquired_at && h.released_at)
884
+ .map((h) => (new Date(h.released_at) - new Date(h.acquired_at)) / 60000)
885
+ .filter((d) => Number.isFinite(d) && d >= 0);
886
+ const avg = durations.length
887
+ ? Math.round(durations.reduce((acc, value) => acc + value, 0) / durations.length)
888
+ : 0;
889
+ document.getElementById("stat-avg").textContent = formatDurationMinutes(avg);
890
+ }
891
+
892
+ function renderActiveLocks(locks) {
893
+ const tbody = document.getElementById("active-locks-body");
894
+ if (!locks.length) {
895
+ tbody.innerHTML =
896
+ '<tr><td colspan="6" class="text-center py-4 text-muted">No active locks</td></tr>';
897
+ return;
898
+ }
899
+
900
+ const now = new Date();
901
+ tbody.innerHTML = locks
902
+ .map((lock) => {
903
+ const acquired = new Date(lock.acquired_at);
904
+ const heldMin = Math.max(0, Math.round((now - acquired) / 60000));
905
+ const isMine = !!(SUPABASE_USER && lock.developer_id === SUPABASE_USER);
906
+ const canRelease = isMine || IS_ADMIN;
907
+ const reason = lock.reason || "No reason";
908
+ const branch = lock.branch_name || "main";
909
+ const safePath = String(lock.file_path || "").replace(/'/g, "\\'");
910
+
911
+ const action = canRelease
912
+ ? '<button class="btn btn-sm btn-outline-danger" onclick="initRelease(\'' + safePath + '\',' + (isMine ? "true" : "false") + ')">' +
913
+ (isMine ? "Release" : "Force Release") +
914
+ "</button>"
915
+ : '<span class="text-muted small">Locked</span>';
916
+
917
+ return (
918
+ "<tr>" +
919
+ "<td><code>" + lock.file_path + "</code></td>" +
920
+ '<td><span class="dev-tag ' + (isMine ? "dev-tag-owner" : "") + '">' +
921
+ lock.developer_id +
922
+ "</span></td>" +
923
+ '<td class="text-muted">' + branch + "</td>" +
924
+ '<td class="text-muted">' + reason + "</td>" +
925
+ '<td class="text-muted">' +
926
+ formatDateTime24(acquired) +
927
+ " (" + formatDurationMinutes(heldMin) + ")</td>" +
928
+ '<td class="text-end">' + action + "</td>" +
929
+ "</tr>"
930
+ );
931
+ })
932
+ .join("");
933
+ }
934
+
935
+ function resetHistory() {
936
+ historyOffset = 0;
937
+ historyHasMore = true;
938
+ historyLoading = false;
939
+ const tbody = document.getElementById("history-body");
940
+ tbody.innerHTML =
941
+ '<tr><td colspan="8" class="text-center py-4 text-muted">Loading history...</td></tr>';
942
+ loadMoreHistory();
943
+ }
944
+
945
+ async function loadMoreHistory() {
946
+ if (historyLoading || !historyHasMore) {
947
+ return;
948
+ }
949
+
950
+ historyLoading = true;
951
+ try {
952
+ const from = historyOffset;
953
+ const to = historyOffset + PAGE_SIZE - 1;
954
+ const { data, error } = await supabaseClient
955
+ .from("file_locks_history")
956
+ .select("*")
957
+ .neq("is_ephemeral", true)
958
+ .order("id", { ascending: false })
959
+ .range(from, to);
960
+
961
+ if (error) {
962
+ throw error;
963
+ }
964
+
965
+ const rows = data || [];
966
+ if (historyOffset === 0 && rows.length === 0) {
967
+ document.getElementById("history-body").innerHTML =
968
+ '<tr><td colspan="8" class="text-center py-4 text-muted">No history found</td></tr>';
969
+ historyHasMore = false;
970
+ return;
971
+ }
972
+
973
+ appendHistoryRows(rows);
974
+ historyOffset += rows.length;
975
+
976
+ if (rows.length < PAGE_SIZE) {
977
+ historyHasMore = false;
978
+ }
979
+ } catch (e) {
980
+ console.error(e);
981
+ } finally {
982
+ historyLoading = false;
983
+ }
984
+ }
985
+
986
+ function appendHistoryRows(rows) {
987
+ const tbody = document.getElementById("history-body");
988
+ const isLoadingPlaceholder = tbody.children.length === 1 &&
989
+ tbody.children[0].textContent.includes("Loading history");
990
+ if (isLoadingPlaceholder) {
991
+ tbody.innerHTML = "";
992
+ }
993
+
994
+ const html = rows
995
+ .map((h) => {
996
+ const acq = h.acquired_at ? new Date(h.acquired_at) : null;
997
+ const rel = h.released_at ? new Date(h.released_at) : null;
998
+ const branch = h.branch_name || "main";
999
+ const reason = h.reason || "No reason";
1000
+ const outcome = h.outcome || "unknown";
1001
+ const duration = acq && rel
1002
+ ? formatDurationMinutes(Math.max(0, Math.round((rel - acq) / 60000)))
1003
+ : "-";
1004
+ const isMine = !!(SUPABASE_USER && h.developer_id === SUPABASE_USER);
1005
+
1006
+ return (
1007
+ "<tr>" +
1008
+ "<td><code>" + h.file_path + "</code></td>" +
1009
+ '<td><span class="dev-tag ' + (isMine ? "dev-tag-owner" : "") + '">' + h.developer_id + "</span></td>" +
1010
+ '<td class="text-muted">' + branch + "</td>" +
1011
+ '<td class="text-muted">' + reason + "</td>" +
1012
+ '<td class="text-muted">' + (acq ? formatDateTime24(acq) : "-") + "</td>" +
1013
+ '<td class="text-muted">' + (rel ? formatDateTime24(rel) : "-") + "</td>" +
1014
+ '<td class="text-muted">' + duration + "</td>" +
1015
+ '<td class="text-muted">' + outcome + "</td>" +
1016
+ "</tr>"
1017
+ );
1018
+ })
1019
+ .join("");
1020
+
1021
+ tbody.insertAdjacentHTML("beforeend", html);
1022
+ }
1023
+
1024
+ function onHistoryScroll(e) {
1025
+ const el = e.target;
1026
+ const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - HISTORY_PREFETCH_GAP_PX;
1027
+ if (nearBottom) {
1028
+ loadMoreHistory();
1029
+ }
1030
+ }
1031
+
1032
+ function initRelease(filePath, isMine) {
1033
+ pendingReleasePath = filePath;
1034
+ document.getElementById("modal-file-path").textContent = filePath;
1035
+ if (isMine) {
1036
+ document.getElementById("modal-title").textContent = "Release File Lock";
1037
+ document.getElementById("modal-description").textContent =
1038
+ "Proceeding will release the lock for this file, allowing other team members to acquire it immediately.";
1039
+ document.getElementById("modal-warning").textContent =
1040
+ "Warning: Verify that you have committed your changes to avoid potential conflicts with other developers.";
1041
+ document.getElementById("confirm-release-btn").textContent = "Confirm Release";
1042
+ } else {
1043
+ document.getElementById("modal-title").textContent = "Force Release File Lock";
1044
+ document.getElementById("modal-description").textContent =
1045
+ "Proceeding will force-release this file lock, allowing other team members to acquire it immediately.";
1046
+ document.getElementById("modal-warning").textContent =
1047
+ "Warning: Force release can interrupt another developer's work. Confirm only if you coordinated with the lock owner.";
1048
+ document.getElementById("confirm-release-btn").textContent = "Confirm Force Release";
1049
+ }
1050
+ releaseModal.show();
1051
+ }
1052
+
1053
+ async function doRelease() {
1054
+ if (!pendingReleasePath || !supabaseClient) {
1055
+ return;
1056
+ }
1057
+ const btn = document.getElementById("confirm-release-btn");
1058
+ btn.disabled = true;
1059
+ try {
1060
+ let query = supabaseClient
1061
+ .from("file_locks")
1062
+ .delete()
1063
+ .eq("file_path", pendingReleasePath);
1064
+
1065
+ if (!IS_ADMIN && SUPABASE_USER) {
1066
+ query = query.eq("developer_id", SUPABASE_USER);
1067
+ }
1068
+
1069
+ const { error } = await query;
1070
+ if (error) {
1071
+ throw error;
1072
+ }
1073
+ releaseModal.hide();
1074
+ await refreshLocks();
1075
+ if (routeFromHash() === "history") {
1076
+ resetHistory();
1077
+ }
1078
+ } catch (e) {
1079
+ alert("Failed to release lock: " + (e.message || String(e)));
1080
+ } finally {
1081
+ btn.disabled = false;
1082
+ pendingReleasePath = null;
1083
+ }
1084
+ }
1085
+
1086
+ async function syncCurrentView() {
1087
+ await refreshLocks();
1088
+ if (routeFromHash() === "history") {
1089
+ resetHistory();
1090
+ }
1091
+ }
1092
+
1093
+ function wireEvents() {
1094
+ document.getElementById("nav-locks").addEventListener("click", () => navigate("locks"));
1095
+ document.getElementById("nav-history").addEventListener("click", () => navigate("history"));
1096
+ document.getElementById("sync-btn").addEventListener("click", syncCurrentView);
1097
+ document.getElementById("confirm-release-btn").addEventListener("click", doRelease);
1098
+ document.getElementById("history-scroll").addEventListener("scroll", onHistoryScroll);
1099
+ window.addEventListener("hashchange", applyRoute);
1100
+ }
1101
+
1102
+ async function init() {
1103
+ releaseModal = new bootstrap.Modal(document.getElementById("releaseModal"));
1104
+ wireEvents();
1105
+
1106
+ if (!SUPABASE_MODE) {
1107
+ showSetup();
1108
+ return;
1109
+ }
1110
+
1111
+ showMain();
1112
+ applyRoute();
1113
+
1114
+ try {
1115
+ await loadSupabaseClient();
1116
+ await refreshLocks();
1117
+ if (routeFromHash() === "history") {
1118
+ resetHistory();
1119
+ }
1120
+ subscribeRealtime();
1121
+ setInterval(refreshLocks, 30000);
1122
+ } catch (e) {
1123
+ console.error("Initialization failed", e);
1124
+ showLocksError("Initialization failed. Check configuration.");
1125
+ }
1126
+ }
1127
+
1128
+ document.addEventListener("DOMContentLoaded", init);
1129
+ </script>
1130
+ </body>
1131
+ </html>