@@ -295,7 +295,7 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
295295 <h3 style="margin-top:12px">Active Locks <span id="lock-count" style="font-weight:400;color:var(--text2)"></span></h3>
296296 <div class="coll-scroll" style="max-height:160px">
297297 <table class="coll-table">
298- <thead><tr><th>Collection</th><th>Pod</th><th>Acquired</th><th>Action</th></tr></thead>
298+ <thead><tr><th>Collection</th><th>Pod</th><th>Acquired</th><th>Expires In</th><th> Action</th></tr></thead>
299299 <tbody id="lock-body"></tbody>
300300 </table>
301301 </div>
@@ -346,6 +346,17 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
346346 </div>
347347</div>
348348
349+ <!-- Danger Zone -->
350+ <div class="card full" style="margin-top:24px;border-color:var(--red)">
351+ <h3 style="color:var(--red)">Danger Zone</h3>
352+ <p style="font-size:11px;color:var(--text2);margin-bottom:12px">These actions are irreversible and will destroy migration state. Use with caution.</p>
353+ <div style="display:flex;gap:12px;flex-wrap:wrap">
354+ <button class="btn" id="btn-clear-mongo" style="padding:6px 16px;font-size:12px;border-color:var(--red);color:var(--red)">Clear MongoDB State</button>
355+ <button class="btn" id="btn-clear-redis" style="padding:6px 16px;font-size:12px;border-color:var(--red);color:var(--red)">Clear Redis State</button>
356+ </div>
357+ <div id="danger-msg" style="margin-top:8px;font-size:11px"></div>
358+ </div>
359+
349360<script>
350361(function() {
351362 'use strict';
@@ -386,6 +397,30 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
386397 $('btn-global-resume').addEventListener('click', function() { doControl('global/resume'); });
387398 $('btn-global-stop').addEventListener('click', function() { doControl('global/stop'); });
388399
400+ // Danger zone buttons
401+ $('btn-clear-mongo').addEventListener('click', function() {
402+ if (!confirm('WARNING: This will permanently delete ALL migration runs, batches, and events from MongoDB. This cannot be undone. Continue?')) return;
403+ var msg = $('danger-msg');
404+ fetch('/control/danger/clear-mongodb', { method: 'POST' })
405+ .then(function(r) { return r.json(); })
406+ .then(function(d) {
407+ msg.textContent = d.ok ? 'MongoDB cleared: ' + d.deletedRecords + ' records deleted' : 'Error: ' + d.error;
408+ msg.style.color = d.ok ? 'var(--green)' : 'var(--red)';
409+ })
410+ .catch(function(e) { msg.textContent = 'Error: ' + e.message; msg.style.color = 'var(--red)'; });
411+ });
412+ $('btn-clear-redis').addEventListener('click', function() {
413+ if (!confirm('WARNING: This will permanently delete ALL migration keys from Redis (cursors, bitmaps, live state, locks, heartbeats). This cannot be undone. Continue?')) return;
414+ var msg = $('danger-msg');
415+ fetch('/control/danger/clear-redis', { method: 'POST' })
416+ .then(function(r) { return r.json(); })
417+ .then(function(d) {
418+ msg.textContent = d.ok ? 'Redis cleared: ' + d.deletedKeys + ' keys deleted' : 'Error: ' + d.error;
419+ msg.style.color = d.ok ? 'var(--green)' : 'var(--red)';
420+ })
421+ .catch(function(e) { msg.textContent = 'Error: ' + e.message; msg.style.color = 'var(--red)'; });
422+ });
423+
389424 function statusClass(s) {
390425 if (s === 'running' || s === 'stopping') return 'status-running';
391426 if (s === 'waiting_for_index') return 'status-waiting_for_index';
@@ -402,7 +437,6 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
402437 }
403438
404439 function truncHash(name) {
405- if (name && name.length > 50) return name.slice(0, 20) + '...' + name.slice(-8);
406440 return name || '-';
407441 }
408442
@@ -421,6 +455,7 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
421455 filtered = cp;
422456 }
423457
458+ filtered.sort(function(a, b) { return (b.estimated || 0) - (a.estimated || 0); });
424459 if (filtered.length > 200) filtered = filtered.slice(0, 200);
425460
426461 var tbody = $('coll-body');
@@ -447,13 +482,9 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
447482
448483 // Collection name
449484 var td1 = document.createElement('td');
450- td1.style.maxWidth = '200px';
451- td1.style.overflow = 'hidden';
452- td1.style.textOverflow = 'ellipsis';
453- td1.style.whiteSpace = 'nowrap';
454485 td1.style.fontSize = '11px';
455- td1.setAttribute('title', c.collection || '') ;
456- td1.textContent = truncHash( c.collection) ;
486+ td1.style.wordBreak = 'break-all' ;
487+ td1.textContent = c.collection || '-' ;
457488 tr.appendChild(td1);
458489
459490 // Status tag
@@ -841,7 +872,7 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
841872 if (locks.length === 0) {
842873 var noLockRow = document.createElement('tr');
843874 var noLockCell = document.createElement('td');
844- noLockCell.setAttribute('colspan', '4 ');
875+ noLockCell.setAttribute('colspan', '5 ');
845876 noLockCell.className = 'muted';
846877 noLockCell.style.textAlign = 'center';
847878 noLockCell.textContent = 'No active locks';
@@ -864,6 +895,20 @@ const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
864895 lkTd3.style.cssText = 'font-size:10px;color:var(--text2)';
865896 lkTd3.textContent = lk.acquiredAt ? new Date(lk.acquiredAt).toLocaleTimeString() : '-';
866897 lkRow.appendChild(lkTd3);
898+ var lkTdTtl = document.createElement('td');
899+ lkTdTtl.style.cssText = 'font-size:10px;color:var(--text2)';
900+ var ttl = lk.ttlSec;
901+ if (ttl > 0) {
902+ var ttlMin = Math.floor(ttl / 60);
903+ var ttlSec = ttl % 60;
904+ lkTdTtl.textContent = ttlMin > 0 ? ttlMin + 'm ' + ttlSec + 's' : ttlSec + 's';
905+ if (ttl < 60) lkTdTtl.style.color = 'var(--red)';
906+ else if (ttl < 120) lkTdTtl.style.color = 'var(--yellow)';
907+ } else {
908+ lkTdTtl.textContent = 'no TTL';
909+ lkTdTtl.style.color = 'var(--red)';
910+ }
911+ lkRow.appendChild(lkTdTtl);
867912 var lkTd4 = document.createElement('td');
868913 var relBtn = document.createElement('button');
869914 relBtn.className = 'btn';
0 commit comments