« Back to History
menu_link_audit.php
|
20260920_164915.php
Initial Domain Snapshot
Copy Code
<?php /* ============================================================================= File: /erp/dev/menu_link_audit.php Title: Menu & Links Audit (Sections + Canonical URLs + Filesystem Auto Scan) Scope: Page-local only • Multi-company aware • No global/base edits ============================================================================= */ header('X-Frame-Options: SAMEORIGIN'); error_reporting(E_ALL); ini_set('display_errors',1); /* ====== CONFIG (you can tweak safely; file-local only) =================== */ $FS_EXCLUDE_DIRS = ['dev','core','modules','vendor','public','api','partials']; // folders to skip while scanning /erp $FS_MAX_FILES = 4000; // safety cap $FS_ALLOW_FILES = ['php']; // which file extensions to consider /* ======================================================================== */ /* [0] Auth (Owner/Admin only) */ require __DIR__ . '/../modules/auth/auth.php'; require_login(['Owner','Admin']); $USER = auth_user(); $COMPANY_ID = (int)$USER['company_id']; $USER_ID = (int)$USER['id']; /* [0a] DB bootstrap (page-local) */ $pdo = $GLOBALS['pdo'] ?? null; if (!$pdo) { require __DIR__ . '/../core/db.php'; } if (!$pdo instanceof PDO) { die('DB not ready'); } /* [0b] Optional header include */ $HAS_HEADER = false; $header_paths = [ __DIR__ . '/../partials/header.php', __DIR__ . '/../partials/_header.php', __DIR__ . '/../header.php', ]; foreach ($header_paths as $hp) { if (is_file($hp)) { $HAS_HEADER = $hp; break; } } /* [1] Helpers */ function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); } function table_exists(PDO $pdo, $name){ $stmt = $pdo->prepare("SHOW TABLES LIKE ?"); $stmt->execute([$name]); return (bool)$stmt->fetchColumn(); } function find_first_existing_table(PDO $pdo, array $candidates){ foreach($candidates as $t){ if (table_exists($pdo, $t)) return $t; } return null; } /* header जैसा absolute-path finder (DOCUMENT_ROOT + /erp mapping) */ function _abs_path_for_url($url){ $url = preg_replace('#\?.*$#','', $url); $url = preg_replace('#\#.*$#','', $url); if (isset($_SERVER['DOCUMENT_ROOT']) && $_SERVER['DOCUMENT_ROOT']!=='') { $abs = realpath(rtrim($_SERVER['DOCUMENT_ROOT'],'/').$url); if ($abs && is_file($abs)) return $abs; } // '/erp/...' → '../...' (relative to this file) if (strpos($url, '/erp/') === 0) { $abs = realpath(__DIR__ . '/..' . substr($url, 4)); if ($abs && is_file($abs)) return $abs; } return null; } /* canonical URL: /erp/<file>.php (case/suffix fix) */ function canonicalize_url($raw){ $p = trim((string)$raw); if ($p === '') return ''; // strip domain + query + hash $p = preg_replace('#^https?://[^/]+#i','',$p); $p = preg_replace('#\?.*$#','', $p); $p = preg_replace('#\#.*$#','', $p); if ($p === '') $p = '/'; if ($p[0] !== '/') $p = '/'.$p; // already a valid file? keep if (_abs_path_for_url($p)) return $p; // basename → slug (drop .php) $base = basename($p); $slug = preg_replace('~\.php$~i','',$base); // try header-style candidates $candidates = [ "/erp/{$slug}.php", "/erp/".strtolower($slug).".php", "/".$slug.".php", "/".strtolower($slug).".php", ]; foreach ($candidates as $c) { if (_abs_path_for_url($c)) return $c; } // if it already looks like '/erp/foo' then append .php and check if (strpos($p,'/erp/')===0 && !preg_match('~\.php$~i',$p)) { $c = $p.'.php'; if (_abs_path_for_url($c)) return $c; } // fallback: minimal normalized path return $p; } /* ===== Filesystem scan (recursive) ====================================== */ /* returns: [canonical_url => ['show_url'=>display_url, 'title'=>human_title]] */ function fs_scan_all_php_under_erp(array $excludeDirs, array $allowExt, int $maxFiles){ $root = realpath(__DIR__ . '/..'); // '/erp' $out = []; if (!$root || !is_dir($root)) return $out; $exts = array_map('strtolower',$allowExt); $skip = array_flip(array_map('strtolower',$excludeDirs)); $count = 0; $it = new RecursiveIteratorIterator( new RecursiveCallbackFilterIterator( new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), function ($current, $key, $iterator) use ($skip) { if ($current->isDir()) { $name = strtolower($current->getFilename()); return !isset($skip[$name]); // skip excluded dirs } return true; } ), RecursiveIteratorIterator::SELF_FIRST ); foreach ($it as $fileInfo){ if ($count >= $maxFiles) break; if (!$fileInfo->isFile()) continue; $ext = strtolower($fileInfo->getExtension() ?: ''); if ($exts && !in_array($ext, $exts, true)) continue; // Make URL relative to /erp $abs = $fileInfo->getPathname(); $rel = substr($abs, strlen($root)); // like '/beam_card_report.php' or '/pissing/piss.php' if ($rel === false) continue; $display = '/erp'.str_replace(DIRECTORY_SEPARATOR,'/',$rel); $key = canonicalize_url($display); $base = basename($display, '.php'); $title = ucwords(str_replace(['_','-'],' ', $base)); $out[$key] = ['show_url'=>$display, 'title'=>$title]; $count++; } return $out; } /* ======================================================================== */ /* [2] Fixed mapping for your menu schema */ $MENU_TABLE = 'nav_menu_items'; $MENU_COLS = [ 'id' => 'id', 'company_id' => 'company_id', 'title' => 'title', 'url' => 'url', 'position' => 'sort_order', 'is_active' => 'is_enabled', 'parent_id' => 'section_id', ]; /* [3] Sections table */ $SECT_TABLE = 'nav_menu_sections'; $SECT_COLS = [ 'id' => 'id', 'company_id' => 'company_id', 'name' => 'name', 'position' => 'sort_order', 'is_active' => 'is_enabled', ]; /* [4] Dev page-catalog table auto-detect (optional) */ $CAT_TABLE = find_first_existing_table($pdo, ['page_catalog','dev_page_catalog','page_meta','dev_pages']); $CAT_COLS = [ 'path' => 'page_name', 'title' => 'title' ]; /* [5] AJAX: add/remove/move + list_sections */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { header('Content-Type: application/json; charset=utf-8'); $action = $_POST['action']; if ($action === 'list_sections') { $rows = []; $q = $pdo->prepare("SELECT `{$SECT_COLS['id']}` AS id, `{$SECT_COLS['name']}` AS name FROM `{$SECT_TABLE}` WHERE `{$SECT_COLS['company_id']}`=? AND COALESCE(`{$SECT_COLS['is_active']}`,1)=1 ORDER BY `{$SECT_COLS['position']}` ASC, `{$SECT_COLS['name']}` ASC"); $q->execute([$COMPANY_ID]); foreach ($q as $r) $rows[] = ['id'=>(int)$r['id'],'name'=>$r['name']]; echo json_encode(['ok'=>true,'data'=>$rows]); exit; } // Common inputs for add/remove/move $url_raw = $_POST['url'] ?? ''; $url = canonicalize_url($url_raw); $title = trim($_POST['title'] ?? ''); $sectId = isset($_POST['section_id']) ? (int)$_POST['section_id'] : null; if ($url === '') { echo json_encode(['ok'=>false,'msg'=>'URL missing']); exit; } // is it already in menu? $st = $pdo->prepare("SELECT `{$MENU_COLS['id']}` AS id FROM `{$MENU_TABLE}` WHERE `{$MENU_COLS['company_id']}`=? AND `{$MENU_COLS['url']}`=? LIMIT 1"); $st->execute([$COMPANY_ID, $url]); $existing_id = (int)$st->fetchColumn(); if ($action === 'add') { if ($existing_id) { echo json_encode(['ok'=>true,'msg'=>'Already in menu']); exit; } // validate/pick section if ($sectId) { $sok = $pdo->prepare("SELECT 1 FROM `{$SECT_TABLE}` WHERE `{$SECT_COLS['company_id']}`=? AND `{$SECT_COLS['id']}`=? AND COALESCE(`{$SECT_COLS['is_active']}`,1)=1"); $sok->execute([$COMPANY_ID, $sectId]); if (!$sok->fetchColumn()) $sectId = null; } if (!$sectId) { $s = $pdo->prepare("SELECT `{$SECT_COLS['id']}` FROM `{$SECT_TABLE}` WHERE `{$SECT_COLS['company_id']}`=? AND COALESCE(`{$SECT_COLS['is_active']}`,1)=1 ORDER BY `{$SECT_COLS['position']}` ASC, `{$SECT_COLS['name']}` ASC LIMIT 1"); $s->execute([$COMPANY_ID]); $sectId = (int)$s->fetchColumn(); if (!$sectId) { echo json_encode(['ok'=>false,'msg'=>'No active section found']); exit; } } if ($title === '') $title = basename($url) ?: $url; // tail position within section $ps = $pdo->prepare("SELECT COALESCE(MAX(`{$MENU_COLS['position']}`),0)+1 FROM `{$MENU_TABLE}` WHERE `{$MENU_COLS['company_id']}`=? AND `{$MENU_COLS['parent_id']}`=?"); $ps->execute([$COMPANY_ID, $sectId]); $pos = (int)$ps->fetchColumn(); $cols = [$MENU_COLS['company_id'],$MENU_COLS['parent_id'],$MENU_COLS['title'],$MENU_COLS['url'],$MENU_COLS['position'],$MENU_COLS['is_active']]; $vals = [$COMPANY_ID, $sectId, $title, $url, $pos, 1]; $sql = "INSERT INTO `{$MENU_TABLE}` (`".implode('`,`',$cols)."`) VALUES (?,?,?,?,?,?)"; $ins = $pdo->prepare($sql); $ins->execute($vals); echo json_encode(['ok'=>true,'msg'=>'Added to menu','section_id'=>$sectId,'canonical_url'=>$url]); exit; } if ($action === 'remove') { if (!$existing_id) { echo json_encode(['ok'=>true,'msg'=>'Not in menu']); exit; } $del = $pdo->prepare("DELETE FROM `{$MENU_TABLE}` WHERE `{$MENU_COLS['id']}`=? AND `{$MENU_COLS['company_id']}`=?"); $del->execute([$existing_id, $COMPANY_ID]); echo json_encode(['ok'=>true,'msg'=>'Removed from menu']); exit; } if ($action === 'move') { if (!$existing_id) { echo json_encode(['ok'=>false,'msg'=>'Item not in menu']); exit; } if (!$sectId) { echo json_encode(['ok'=>false,'msg'=>'Target section required']); exit; } // validate target section $sok = $pdo->prepare("SELECT 1 FROM `{$SECT_TABLE}` WHERE `{$SECT_COLS['company_id']}`=? AND `{$SECT_COLS['id']}`=?"); $sok->execute([$COMPANY_ID, $sectId]); if (!$sok->fetchColumn()) { echo json_encode(['ok'=>false,'msg'=>'Invalid section']); exit; } // new tail position within target section $ps = $pdo->prepare("SELECT COALESCE(MAX(`{$MENU_COLS['position']}`),0)+1 FROM `{$MENU_TABLE}` WHERE `{$MENU_COLS['company_id']}`=? AND `{$MENU_COLS['parent_id']}`=?"); $ps->execute([$COMPANY_ID, $sectId]); $pos = (int)$ps->fetchColumn(); $up = $pdo->prepare("UPDATE `{$MENU_TABLE}` SET `{$MENU_COLS['parent_id']}`=?, `{$MENU_COLS['position']}`=? WHERE `{$MENU_COLS['id']}`=? AND `{$MENU_COLS['company_id']}`=?"); $up->execute([$sectId, $pos, $existing_id, $COMPANY_ID]); echo json_encode(['ok'=>true,'msg'=>'Section updated']); exit; } echo json_encode(['ok'=>false,'msg'=>'Unknown action']); exit; } /* [6] Fetch sections (for UI dropdowns) */ $SECTIONS = []; $q = $pdo->prepare("SELECT `{$SECT_COLS['id']}` AS id, `{$SECT_COLS['name']}` AS name FROM `{$SECT_TABLE}` WHERE `{$SECT_COLS['company_id']}`=? AND COALESCE(`{$SECT_COLS['is_active']}`,1)=1 ORDER BY `{$SECT_COLS['position']}` ASC, `{$SECT_COLS['name']}` ASC"); $q->execute([$COMPANY_ID]); foreach ($q as $r) { $SECTIONS[(int)$r['id']] = (string)$r['name']; } /* [7] Dev catalog (optional) */ $catalog = []; // key = canonical, value = ['show_url'=>..., 'title'=>...] if ($CAT_TABLE) { $cols = []; foreach($pdo->query("SHOW COLUMNS FROM `{$CAT_TABLE}`") as $r){ $cols[strtolower($r['Field'])] = $r['Field']; } $pathCol = $cols['page_name'] ?? ($cols['path'] ?? ($cols['page'] ?? 'page_name')); $titleCol = $cols['title'] ?? null; $sql = "SELECT DISTINCT `$pathCol`".($titleCol? ", `$titleCol`":"")." FROM `{$CAT_TABLE}` ORDER BY 1"; foreach($pdo->query($sql) as $r){ $display = trim((string)$r[$pathCol]); if ($display==='') continue; $key = canonicalize_url($display); $title = $titleCol ? trim((string)$r[$titleCol]) : ''; $catalog[$key] = ['show_url'=>$display, 'title'=>$title]; } } /* [7.5] Filesystem AUTO scan: /erp (recursive), union into catalog */ $fsMap = fs_scan_all_php_under_erp($FS_EXCLUDE_DIRS, $FS_ALLOW_FILES, $FS_MAX_FILES); // [key => ['show_url','title']] foreach ($fsMap as $key => $meta) { if (!isset($catalog[$key])) { $catalog[$key] = $meta; // bring fs file into list } } /* [8] Current menu items (JOIN sections), key by canonical url */ $menuItems = []; // key = canonical url $sql = "SELECT i.`{$MENU_COLS['title']}` AS title, i.`{$MENU_COLS['url']}` AS url, i.`{$MENU_COLS['parent_id']}` AS section_id, s.`{$SECT_COLS['name']}` AS section_name FROM `{$MENU_TABLE}` i LEFT JOIN `{$SECT_TABLE}` s ON s.`{$SECT_COLS['id']}` = i.`{$MENU_COLS['parent_id']}` WHERE i.`{$MENU_COLS['company_id']}` = :cid AND COALESCE(i.`{$MENU_COLS['is_active']}`,1)=1 ORDER BY i.`{$MENU_COLS['position']}`"; $st = $pdo->prepare($sql); $st->execute([':cid'=>$COMPANY_ID]); foreach ($st as $r){ $display = trim((string)$r['url']); // DB में जो है (display) $key = canonicalize_url($display); // compare के लिए canonical $menuItems[$key] = [ 'title' => trim((string)$r['title']), 'display_url' => $display, 'section_id' => (int)$r['section_id'], 'section_name' => (string)$r['section_name'], ]; } /* [9] Union rows (key = canonical) */ $rows = []; foreach ($catalog as $key => $meta){ $in = isset($menuItems[$key]); $rows[$key] = [ 'key_url' => $key, // canonical (hidden) 'url' => $meta['show_url'], // display जैसा आया (catalog/fs/db) 'title' => $meta['title'] ?: ($in ? $menuItems[$key]['title'] : (basename($key) ?: $key)), 'in_menu' => $in, 'section' => $in ? ($menuItems[$key]['section_name'] ?: '—') : '—', 'section_id' => $in ? $menuItems[$key]['section_id'] : 0, ]; } foreach ($menuItems as $key => $mi){ if (!isset($rows[$key])){ $rows[$key] = [ 'key_url' => $key, 'url' => $mi['display_url'], 'title' => $mi['title'] ?: (basename($key) ?: $key), 'in_menu' => true, 'section' => $mi['section_name'] ?: '—', 'section_id' => (int)$mi['section_id'], ]; } } /* [10] HTML UI */ ?><!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>Menu & Links Audit</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> :root { --bg:#fff; --fg:#222; --muted:#666; --ok:#157347; --warn:#b02a37; --chip:#e7f5ee; --chip2:#f6e7e7; --line:#eee; } body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Arial; background: var(--bg); color: var(--fg); margin:0; } header { padding:14px 18px; border-bottom:1px solid var(--line); display:flex; gap:12px; align-items:center; flex-wrap:wrap;} header h1 { font-size:18px; margin:0; } .note { font-size:13px; color:var(--muted); } .bar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; } input[type="search"]{ padding:8px 10px; font-size:14px; border:1px solid #ddd; border-radius:10px; min-width:260px; } select { padding:6px 8px; border:1px solid #ddd; border-radius:8px; font-size:13px; } table { width:100%; border-collapse: collapse; margin-top:10px; } th, td { padding:10px 12px; border-bottom:1px solid var(--line); vertical-align: middle; } th { text-align:left; font-size:12px; text-transform: uppercase; letter-spacing:.06em; color:#444; } .chip { display:inline-block; padding:4px 8px; font-size:12px; border-radius:999px; background:var(--chip); color:var(--ok); border:1px solid #cfe9dc; } .chip.off { background:var(--chip2); color:var(--warn); border-color:#f0d1d1; } .btn { padding:6px 10px; border-radius:8px; border:1px solid #ddd; background:#fafafa; cursor:pointer; font-size:13px; } .btn.ok { border-color:#cfe9dc; background:#e9f7ef; color:#0a6f3c; } .btn.danger { border-color:#f0d1d1; background:#fdecec; color:#a52834; } .btn:disabled { opacity:.5; cursor:not-allowed; } .wrap { max-width:1100px; margin:0 auto; padding:16px; } .right { margin-left:auto; } .small { font-size:12px; color:#666; } .url { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; } .badge { margin-left:6px; font-size:11px; color:#888; border:1px dashed #ddd; padding:2px 6px; border-radius:999px; } </style> </head> <body> <?php if ($HAS_HEADER) { include $HAS_HEADER; } ?> <div class="wrap"> <header> <h1>Menu & Links Audit</h1> <span class="note">Company #<?= (int)$COMPANY_ID ?></span> <span class="note">Menu: <strong><?= h($MENU_TABLE) ?></strong></span> <span class="note">Sections: <strong><?= h($SECT_TABLE) ?></strong></span> <span class="note">Catalog: <strong><?= $CAT_TABLE ? h($CAT_TABLE) : '— none —' ?></strong></span> <span class="note">FS Scan: <strong>/erp (recursive)</strong></span> <div class="right small">Search, filter, add/remove, move section</div> </header> <div class="bar" style="margin-top:12px;"> <input id="q" type="search" placeholder="Search title or URL..." /> <button class="btn" id="btnShowAll">Show all</button> <button class="btn" id="btnOnlyMenu">Only: In Menu</button> <button class="btn" id="btnOnlyNot">Only: Not in Menu</button> <div class="right small"> Total: <strong id="countTotal"><?= count($rows) ?></strong> </div> </div> <table id="grid"> <thead> <tr> <th style="width:34%">Title</th> <th style="width:34%">URL / Path</th> <th style="width:12%">Menu</th> <th style="width:12%">Section</th> <th style="width:8%">Actions</th> </tr> </thead> <tbody> <?php foreach($rows as $r): ?> <?php $in = $r['in_menu']; $title = $r['title'] ?: '—'; $urlShow= $r['url']; // display-only $keyUrl = $r['key_url']; // canonical (hidden) $secId = (int)$r['section_id']; ?> <tr data-title="<?= h(mb_strtolower($title)) ?>" data-url="<?= h(mb_strtolower($keyUrl)) ?>" data-in="<?= $in ? '1':'0' ?>"> <td><?= h($title) ?></td> <td class="url"> <?= h($urlShow) ?> <?php if ($urlShow !== $keyUrl): ?> <span class="badge" title="canonical"><?= h($keyUrl) ?></span> <?php endif; ?> </td> <td><?= $in ? '<span class="chip">IN MENU</span>' : '<span class="chip off">NOT IN MENU</span>' ?></td> <td> <div class="row-section" data-section="<?= $secId ?>"> <select class="sect"> <?php foreach($SECTIONS as $sid=>$sname): ?> <option value="<?= (int)$sid ?>" <?= $in && $sid===$secId?'selected':'' ?>><?= h($sname) ?></option> <?php endforeach; ?> </select> <?php if ($in): ?> <button class="btn ok act-move" data-url="<?= h($keyUrl) ?>">Move</button> <?php endif; ?> </div> </td> <td> <button class="btn ok act-add" <?= $in ? 'disabled':'' ?> data-url="<?= h($keyUrl) ?>" data-title="<?= h($title) ?>">Add</button> <button class="btn danger act-rem" <?= $in ? '':'disabled' ?> data-url="<?= h($keyUrl) ?>">Remove</button> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <script> (function(){ const q = document.getElementById('q'); const grid = document.getElementById('grid').querySelector('tbody'); const btnAll = document.getElementById('btnShowAll'); const btnOnlyMenu = document.getElementById('btnOnlyMenu'); const btnOnlyNot = document.getElementById('btnOnlyNot'); const countTotal = document.getElementById('countTotal'); function applyFilter(){ const term = (q.value||'').trim().toLowerCase(); const rows = grid.querySelectorAll('tr'); let shown = 0; rows.forEach(tr=>{ const t = tr.getAttribute('data-title')||''; const u = tr.getAttribute('data-url')||''; const hit = (!term) || t.includes(term) || u.includes(term); tr.style.display = hit ? '' : 'none'; if (hit) shown++; }); countTotal.textContent = shown; } q.addEventListener('input', applyFilter); btnAll.addEventListener('click', ()=>{ q.value=''; applyFilter(); }); btnOnlyMenu.addEventListener('click', ()=>{ const rows = grid.querySelectorAll('tr'); let shown=0; rows.forEach(tr=>{ const inmenu = tr.getAttribute('data-in') === '1'; tr.style.display = inmenu ? '' : 'none'; if (inmenu) shown++; }); countTotal.textContent = shown; }); btnOnlyNot.addEventListener('click', ()=>{ const rows = grid.querySelectorAll('tr'); let shown=0; rows.forEach(tr=>{ const inmenu = tr.getAttribute('data-in') === '1'; tr.style.display = (!inmenu) ? '' : 'none'; if (!inmenu) shown++; }); countTotal.textContent = shown; }); function post(data){ return fetch(location.href, { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body: new URLSearchParams(data) }).then(r=>r.json()); } grid.addEventListener('click', async (ev)=>{ const btn = ev.target.closest('button'); if (!btn) return; const tr = ev.target.closest('tr'); const url = btn.getAttribute('data-url'); // canonical const title = btn.getAttribute('data-title') || tr.children[0].textContent.trim(); const sectSel = tr.querySelector('select.sect'); const section_id = sectSel ? parseInt(sectSel.value,10) : 0; if (btn.classList.contains('act-add')){ btn.disabled = true; const res = await post({action:'add', url, title, section_id}); alert(res.msg || (res.ok ? 'Done' : 'Failed')); if (res.ok){ tr.setAttribute('data-in','1'); const chip = tr.querySelector('.chip'); chip.className = 'chip'; chip.textContent = 'IN MENU'; tr.querySelector('.act-add').disabled = true; tr.querySelector('.act-rem').disabled = false; const sectDiv = tr.querySelector('.row-section'); if (sectDiv && !tr.querySelector('.act-move')) { const moveBtn = document.createElement('button'); moveBtn.className = 'btn ok act-move'; moveBtn.setAttribute('data-url', url); moveBtn.textContent = 'Move'; sectDiv.appendChild(moveBtn); } } else { btn.disabled = false; } } if (btn.classList.contains('act-rem')){ if (!confirm('Remove from menu?')) return; btn.disabled = true; const res = await post({action:'remove', url}); alert(res.msg || (res.ok ? 'Done' : 'Failed')); if (res.ok){ tr.setAttribute('data-in','0'); const chip = tr.querySelector('.chip'); chip.className = 'chip off'; chip.textContent = 'NOT IN MENU'; tr.querySelector('.act-add').disabled = false; tr.querySelector('.act-rem').disabled = true; const moveBtn = tr.querySelector('.act-move'); if (moveBtn) moveBtn.remove(); } else { btn.disabled = false; } } if (btn.classList.contains('act-move')){ btn.disabled = true; const res = await post({action:'move', url, section_id}); alert(res.msg || (res.ok ? 'Done' : 'Failed')); if (!res.ok) btn.disabled = false; else setTimeout(()=>{ btn.disabled = false; }, 250); } }); })(); </script> </body> </html>