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.
- collab/__init__.py +77 -0
- collab/__main__.py +11 -0
- collab_runtime-0.2.9.dist-info/METADATA +218 -0
- collab_runtime-0.2.9.dist-info/RECORD +82 -0
- collab_runtime-0.2.9.dist-info/WHEEL +5 -0
- collab_runtime-0.2.9.dist-info/entry_points.txt +3 -0
- collab_runtime-0.2.9.dist-info/licenses/LICENSE +21 -0
- collab_runtime-0.2.9.dist-info/top_level.txt +10 -0
- scripts/cleanup.py +395 -0
- scripts/collab_git_hook.py +190 -0
- scripts/format_code.py +594 -0
- scripts/generate_tests.py +560 -0
- scripts/validate_code.py +1397 -0
- src/__init__.py +4 -0
- src/dashboard/index.html +1131 -0
- src/live_locks_watcher.py +1982 -0
- src/lock_client.py +4268 -0
- src/logging_config.py +259 -0
- src/main.py +436 -0
- tests/backend/__init__.py +0 -0
- tests/backend/functional/__init__.py +0 -0
- tests/backend/functional/test_package_imports.py +43 -0
- tests/backend/integration/__init__.py +0 -0
- tests/backend/integration/test_cli_contract_parity.py +220 -0
- tests/backend/performance/__init__.py +0 -0
- tests/backend/reliability/__init__.py +0 -0
- tests/backend/security/__init__.py +0 -0
- tests/backend/unit/live_locks_watcher/__init__.py +5 -0
- tests/backend/unit/live_locks_watcher/_helpers.py +123 -0
- tests/backend/unit/live_locks_watcher/conftest.py +18 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_dashboard.py +188 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_developer.py +56 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_graceful_shutdown.py +459 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_main.py +1925 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_module.py +187 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_multi_session.py +320 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_notify.py +67 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_parsing.py +155 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_process_helpers.py +684 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_processing.py +173 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_prompt_abort.py +71 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_reconcile.py +516 -0
- tests/backend/unit/live_locks_watcher/test_live_locks_watcher_scan.py +296 -0
- tests/backend/unit/lock_client/__init__.py +1 -0
- tests/backend/unit/lock_client/_helpers.py +132 -0
- tests/backend/unit/lock_client/test_lock_client_acquire.py +214 -0
- tests/backend/unit/lock_client/test_lock_client_active.py +104 -0
- tests/backend/unit/lock_client/test_lock_client_api.py +63 -0
- tests/backend/unit/lock_client/test_lock_client_cli.py +682 -0
- tests/backend/unit/lock_client/test_lock_client_daemon.py +3730 -0
- tests/backend/unit/lock_client/test_lock_client_dashboard.py +438 -0
- tests/backend/unit/lock_client/test_lock_client_discover.py +241 -0
- tests/backend/unit/lock_client/test_lock_client_force_release.py +354 -0
- tests/backend/unit/lock_client/test_lock_client_helper_branches.py +1890 -0
- tests/backend/unit/lock_client/test_lock_client_history.py +301 -0
- tests/backend/unit/lock_client/test_lock_client_isolation.py +316 -0
- tests/backend/unit/lock_client/test_lock_client_pid.py +75 -0
- tests/backend/unit/lock_client/test_lock_client_reconcile.py +464 -0
- tests/backend/unit/lock_client/test_lock_client_release.py +77 -0
- tests/backend/unit/lock_client/test_lock_client_shutdown.py +1110 -0
- tests/backend/unit/lock_client/test_lock_client_utils.py +474 -0
- tests/backend/unit/lock_client/test_lock_client_watch.py +866 -0
- tests/backend/unit/scripts/__init__.py +1 -0
- tests/backend/unit/scripts/_helpers.py +42 -0
- tests/backend/unit/scripts/test_cleanup.py +285 -0
- tests/backend/unit/scripts/test_collab_git_hook.py +280 -0
- tests/backend/unit/scripts/test_collab_git_hook_ported.py +50 -0
- tests/backend/unit/scripts/test_format_code.py +368 -0
- tests/backend/unit/scripts/test_format_code_ported.py +177 -0
- tests/backend/unit/scripts/test_generate_tests.py +305 -0
- tests/backend/unit/scripts/test_hook_templates.py +357 -0
- tests/backend/unit/scripts/test_setup_hook_overlay.py +95 -0
- tests/backend/unit/scripts/test_validate_code.py +867 -0
- tests/backend/unit/scripts/test_validate_code_ported.py +237 -0
- tests/backend/unit/test_entrypoints_main_run.py +83 -0
- tests/backend/unit/test_logging_config.py +529 -0
- tests/backend/unit/test_main_watch_pid_file.py +278 -0
- tests/conftest.py +167 -0
- tests/frontend/__init__.py +0 -0
- tests/frontend/jest/__init__.py +0 -0
- tests/frontend/playwright/__init__.py +0 -0
- tests/packaging/test_smoke_install.py +76 -0
src/dashboard/index.html
ADDED
|
@@ -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>
|