|
220 | 220 | border-radius:var(--radius-sm); color:#2ecc71; font-size:11px; padding:3px 10px; cursor:pointer; } |
221 | 221 | .btn-save-list:hover { background:rgba(46,204,113,.25); } |
222 | 222 | .save-ok { font-size:11px; color:#2ecc71; opacity:0; transition:opacity .4s; margin-left:4px; } |
| 223 | + .dry-run-wrap { display:flex; align-items:center; gap:6px; font-size:12px; |
| 224 | + color:var(--text2); white-space:nowrap; } |
| 225 | + .dry-run-wrap input[type=checkbox] { accent-color:#f59e0b; width:14px; height:14px; cursor:pointer; } |
| 226 | + .dry-banner { display:none; background:rgba(245,158,11,.12); border:.5px solid rgba(245,158,11,.4); |
| 227 | + border-radius:var(--radius-sm); padding:7px 14px; margin-bottom:8px; |
| 228 | + font-size:12px; color:#f59e0b; } |
| 229 | + .dry-banner.show { display:block; } |
223 | 230 | .btn-save-del { background:#7a5200; border:none; |
224 | 231 | border-radius:var(--radius-sm); color:#f5c842; font-size:13px; font-weight:500; |
225 | 232 | padding:7px 13px; cursor:pointer; white-space:nowrap; } |
|
242 | 249 | <svg viewBox="0 0 24 24"><path d="M19.36 2.72 20.78 4.14l-1.4 1.41 1.41 1.41-4.24 4.24-1.41-1.41-6.89 6.9a4 4 0 0 1-1.2 4.71l-2.83 2.12-1.41-1.41 2.12-2.83a2 2 0 0 0 .27-2.1l-.27-.42L3 15.36l1.41-1.41 1.42 1.42 5.65-5.66-1.41-1.41 4.24-4.24 1.41 1.41z"/></svg> |
243 | 250 | </div> |
244 | 251 | <div> |
245 | | - <div class="topbar-title">Orphan Entity Cleaner <span style="font-size:11px;font-weight:400;color:var(--text2);margin-left:6px">v2.4.1</span></div> |
| 252 | + <div class="topbar-title">Orphan Entity Cleaner <span style="font-size:11px;font-weight:400;color:var(--text2);margin-left:6px">v2.4.2</span></div> |
246 | 253 | <div class="topbar-sub">Home Assistant Integration — orphan entity cleanup</div> |
247 | 254 | </div> |
248 | 255 | <div class="topbar-spacer"></div> |
|
308 | 315 | <button id="btn-desel" onclick="deselAll()" disabled>Deselect</button> |
309 | 316 | <input class="search" type="text" placeholder="Filter by entity_id or platform…" |
310 | 317 | id="filter" oninput="applyFilter()" disabled> |
| 318 | + <label class="dry-run-wrap" title="Simulate deletion without removing anything"> |
| 319 | + <input type="checkbox" id="dry-run-toggle" onchange="onDryRunToggle()"> Dry run |
| 320 | + </label> |
311 | 321 | <button class="btn-danger" id="btn-del" onclick="confirmDelete()" disabled>Delete selected</button> |
312 | 322 | <button class="btn-save-del" id="btn-save-del" onclick="confirmSaveDelete()" disabled title="Save a backup JSON before deleting">Save & Delete</button> |
313 | 323 | </div> |
| 324 | + <div class="dry-banner" id="dry-banner"> |
| 325 | + 🔍 <strong>Dry run mode</strong> — deletion will be simulated, nothing will actually be removed. |
| 326 | + </div> |
314 | 327 |
|
315 | 328 | <!-- Table card --> |
316 | 329 | <div class="card"> |
@@ -534,11 +547,65 @@ <h2>Confirm deletion</h2> |
534 | 547 |
|
535 | 548 | function confirmDelete() { |
536 | 549 | if (!selected.size) return; |
| 550 | + if (isDryRun()) { doDryRun(); return; } |
537 | 551 | document.getElementById('modal-msg').innerHTML = |
538 | 552 | 'You are about to delete <strong>'+selected.size+' entities</strong> from the Home Assistant registry.<br>' |
539 | 553 | +'This operation is <strong>irreversible</strong>. Continue?'; |
540 | 554 | document.getElementById('overlay').classList.add('show'); |
541 | 555 | } |
| 556 | +function onDryRunToggle() { |
| 557 | + const dry = document.getElementById('dry-run-toggle').checked; |
| 558 | + document.getElementById('dry-banner').classList.toggle('show', dry); |
| 559 | + document.getElementById('btn-del').textContent = dry ? 'Simulate' : 'Delete selected'; |
| 560 | + document.getElementById('btn-save-del').style.display = dry ? 'none' : ''; |
| 561 | +} |
| 562 | + |
| 563 | +function isDryRun() { |
| 564 | + return document.getElementById('dry-run-toggle').checked; |
| 565 | +} |
| 566 | + |
| 567 | +async function doDryRun() { |
| 568 | + const ids = [...selected]; |
| 569 | + addLog('🔍 Dry run: simulating deletion of '+ids.length+' entities…', 'info'); |
| 570 | + setStatus('Dry run in progress…'); |
| 571 | + try { |
| 572 | + const r = await fetch('/api/orphan_cleaner/delete', { |
| 573 | + method: 'POST', headers: {'Content-Type':'application/json'}, |
| 574 | + body: JSON.stringify({ entity_ids: ids, dry_run: true }) |
| 575 | + }); |
| 576 | + if (!r.ok) throw new Error('HTTP '+r.status); |
| 577 | + const d = await r.json(); |
| 578 | + addLog('🔍 Dry run complete: '+d.deleted_count+' would be deleted, '+d.failed_count+' would fail.', 'ok'); |
| 579 | + setStatus('Dry run complete — nothing was deleted.'); |
| 580 | + ids.forEach(id => { |
| 581 | + document.querySelectorAll('tr').forEach(row => { |
| 582 | + const eid = row.querySelector('.eid'); |
| 583 | + if (eid && eid.textContent === id) { |
| 584 | + row.style.outline = '1.5px solid #f59e0b'; |
| 585 | + row.style.opacity = '0.55'; |
| 586 | + if (!row.querySelector('.badge-dry')) { |
| 587 | + const b = document.createElement('span'); |
| 588 | + b.className = 'badge-dry'; |
| 589 | + b.textContent = 'would delete'; |
| 590 | + b.style.cssText = 'background:#78350f;color:#fcd34d;border-radius:4px;padding:1px 6px;font-size:10px;margin-left:5px;'; |
| 591 | + eid.after(b); |
| 592 | + } |
| 593 | + } |
| 594 | + }); |
| 595 | + }); |
| 596 | + setTimeout(() => { |
| 597 | + document.querySelectorAll('tr').forEach(row => { |
| 598 | + row.style.outline = ''; |
| 599 | + row.style.opacity = ''; |
| 600 | + row.querySelectorAll('.badge-dry').forEach(b => b.remove()); |
| 601 | + }); |
| 602 | + }, 6000); |
| 603 | + } catch(err) { |
| 604 | + addLog('Dry run error: '+err.message, 'err'); |
| 605 | + setStatus('Dry run failed.', 'err'); |
| 606 | + } |
| 607 | +} |
| 608 | + |
542 | 609 | function showDiff() { |
543 | 610 | const banner = document.getElementById('diff-banner'); |
544 | 611 | if (!previousOrphans.length) { banner.classList.remove('show'); return; } |
|
0 commit comments