docs: add project README and initialize repository

This commit is contained in:
2026-05-30 15:36:11 +08:00
commit 566fc822d5
39 changed files with 7124 additions and 0 deletions
+468
View File
@@ -0,0 +1,468 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PortFlow - 系统进程与网络监视器</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
*, *::before, *::after {
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #020617;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #334155;
}
@keyframes radar-pulse {
0% { transform: scale(0.95); opacity: 0.8; box-shadow: 0 0 30px rgba(79, 70, 229, 0.15); }
50% { transform: scale(1.05); opacity: 0.5; box-shadow: 0 0 50px rgba(79, 70, 229, 0.3); }
100% { transform: scale(0.95); opacity: 0.8; box-shadow: 0 0 30px rgba(79, 70, 229, 0.15); }
}
.radar-animate {
animation: radar-pulse 2s infinite ease-in-out;
}
</style>
</head>
<body class="bg-slate-50 text-slate-800 dark:bg-slate-950 dark:text-slate-100 h-screen w-screen overflow-hidden font-sans select-none flex flex-col border border-slate-200 dark:border-slate-800 rounded-lg">
<div id="splash-screen" data-tauri-drag-region class="absolute inset-0 z-50 bg-slate-100 flex flex-col items-center justify-center text-slate-800 transition-all duration-500 ease-in-out">
<div class="flex flex-col items-center pointer-events-none">
<div class="relative w-20 h-20 bg-white rounded-full flex items-center justify-center border border-slate-200 shadow-md mb-6 radar-animate">
<i class="fa-solid fa-network-wired text-3xl text-indigo-500 animate-pulse"></i>
<div class="absolute inset-0 border-2 border-dashed border-indigo-500/20 rounded-full animate-spin" style="animation-duration: 8s;"></div>
</div>
<h1 class="text-2xl font-black tracking-widest text-slate-800 font-sans uppercase" id="splash-app-name">PORTFLOW</h1>
<p class="text-xs text-indigo-600 font-medium mt-2 tracking-wider">系统端口与映像进程分析内核</p>
<div class="w-48 h-1 bg-slate-200 rounded-full mt-8 overflow-hidden">
<div class="h-full w-1/2 bg-gradient-to-r from-indigo-500 to-indigo-400 rounded-full animate-[shimmer_1.5s_infinite_linear]" style="animation-name: progress-shimmer;">
<style>
@keyframes progress-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(200%); }
}
</style>
</div>
</div>
<p class="text-[11px] text-slate-400 font-medium mt-3" id="splash-status">正在下钻底层网络套接字,映射物理端口映像...</p>
</div>
</div>
<div id="custom-titlebar" data-tauri-drag-region class="h-10 w-full bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 flex justify-between items-center pl-4 shrink-0 select-none cursor-default">
<div class="flex items-center space-x-2 text-xs font-medium text-slate-500 dark:text-slate-400 pointer-events-none">
<i class="fa-solid fa-network-wired text-indigo-500"></i>
<span id="titlebar-app-name">PortFlow 进程管理器</span>
</div>
<div class="flex items-center h-full text-slate-500 dark:text-slate-400 text-xs">
<button onclick="windowMinimize()" class="w-12 h-full hover:bg-slate-200 dark:hover:bg-slate-800 flex items-center justify-center transition-colors duration-75 cursor-pointer" title="最小化">
<i class="fa-solid fa-minus"></i>
</button>
<button onclick="windowMaximize()" class="w-12 h-full hover:bg-slate-200 dark:hover:bg-slate-800 flex items-center justify-center transition-colors duration-75 cursor-pointer" title="最大化">
<i id="max-icon" class="fa-regular fa-square"></i>
</button>
<button onclick="windowClose()" class="w-12 h-full hover:bg-rose-600 hover:text-white flex items-center justify-center transition-colors duration-75 cursor-pointer" title="关闭">
<i class="fa-solid fa-xmark text-sm"></i>
</button>
</div>
</div>
<header class="border-b border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900/50 px-6 py-3.5 flex justify-between items-center shrink-0 shadow-xs">
<div class="flex items-center space-x-3">
<div class="bg-slate-800 dark:bg-indigo-600 p-2 rounded-lg text-white">
<i class="fa-solid fa-chart-pie text-lg"></i>
</div>
<div>
<h1 class="text-lg font-bold tracking-tight text-slate-900 dark:text-white flex items-center gap-2" id="header-app-name">PortFlow 进程管理器</h1>
<p class="text-xs text-slate-400 dark:text-slate-500">实时监控系统端口与映像进程分析</p>
</div>
</div>
<div class="flex items-center space-x-3">
<button onclick="toggleTheme()" class="w-9 h-9 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:hover:bg-slate-800 flex items-center justify-center cursor-pointer shadow-2xs" title="切换主题">
<i id="theme-icon" class="fa-solid fa-sun text-amber-500"></i>
</button>
<span class="flex items-center text-xs bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 px-2.5 py-1 rounded border border-indigo-500/20 font-mono" id="app-version-badge">
<span class="w-1.5 h-1.5 bg-indigo-500 rounded-full mr-2 animate-pulse"></span>版本: 获取中...
</span>
</div>
</header>
<main class="max-w-7xl w-full mx-auto p-4 flex-1 min-h-0 flex flex-col space-y-4 overflow-hidden">
<section class="grid grid-cols-2 md:grid-cols-4 gap-3 shrink-0">
<div class="bg-white dark:bg-slate-900 p-3 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs flex items-center justify-between">
<div>
<p class="text-xs text-slate-400 dark:text-slate-500">网络套接字总数</p>
<p class="text-xl font-bold mt-0.5 text-slate-900 dark:text-white" id="total-ports">0</p>
</div>
<i class="fa-solid fa-chart-line text-indigo-500 opacity-30 text-xl"></i>
</div>
<div class="bg-white dark:bg-slate-900 p-3 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs flex items-center justify-between">
<div>
<p class="text-xs text-slate-400 dark:text-slate-500">TCP 监听级</p>
<p class="text-xl font-bold mt-0.5 text-blue-600 dark:text-blue-400" id="tcp-count">0</p>
</div>
<i class="fa-solid fa-arrow-right-arrows-left text-blue-500 opacity-30 text-xl"></i>
</div>
<div class="bg-white dark:bg-slate-900 p-3 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs flex items-center justify-between">
<div>
<p class="text-xs text-slate-400 dark:text-slate-500">UDP 数据报</p>
<p class="text-xl font-bold mt-0.5 text-purple-600 dark:text-purple-400" id="udp-count">0</p>
</div>
<i class="fa-solid fa-bolt text-purple-500 opacity-30 text-xl"></i>
</div>
<div class="bg-white dark:bg-slate-900 p-3 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs flex items-center justify-between">
<div>
<p class="text-xs text-slate-400 dark:text-slate-500">已终止进程数</p>
<p class="text-xl font-bold mt-0.5 text-rose-600 dark:text-rose-400" id="killed-count">0</p>
</div>
<i class="fa-solid fa-ban text-rose-500 opacity-30 text-xl"></i>
</div>
</section>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 flex-1 min-h-0 items-stretch">
<div class="lg:col-span-2 flex flex-col space-y-4 min-h-0" id="left-panel">
<div class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs shrink-0 flex gap-2 items-center">
<div class="relative flex-1">
<i class="fa-solid fa-filter absolute left-3 top-3 text-slate-400 text-xs"></i>
<input id="search-input" type="text" placeholder="输入端口号或进程名称过滤 (例如: 8080 或 node)..." class="w-full bg-slate-50 border border-slate-200 dark:bg-slate-950 dark:border-slate-800 rounded-md pl-9 pr-4 py-1.5 text-sm placeholder-slate-400 focus:outline-none focus:border-indigo-500 transition-all">
</div>
<button onclick="throttledLoadPorts()" class="text-xs bg-slate-100 hover:bg-slate-200 dark:bg-slate-800 dark:hover:bg-slate-700 px-3 py-2 border border-slate-200 dark:border-slate-700 rounded-md font-medium cursor-pointer flex items-center gap-1.5 shrink-0 transition-all active:scale-95">
<i id="refresh-icon" class="fa-solid fa-arrows-rotate transition-transform duration-500"></i>
<span>刷新列表</span>
</button>
</div>
<div class="bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs flex-1 flex flex-col min-h-0 overflow-hidden">
<div class="overflow-y-auto flex-1 custom-scrollbar">
<table class="w-full text-left border-collapse table-fixed">
<thead class="sticky top-0 bg-slate-100 dark:bg-slate-800 z-10 border-b border-slate-200 dark:border-slate-700 shadow-xs">
<tr class="text-slate-600 dark:text-slate-300 text-xs font-semibold">
<th class="py-2.5 px-4 w-20">类型</th>
<th class="py-2.5 px-4 w-28">本地端口</th>
<th class="py-2.5 px-4">关联映像名称 (Process)</th>
<th class="py-2.5 px-4 w-24">PID</th>
<th class="py-2.5 px-4 w-32">状态</th>
<th class="py-2.5 px-4 w-24 text-center">操作</th>
</tr>
</thead>
<tbody id="port-table-body" class="divide-y divide-slate-100 dark:divide-slate-800/60 text-xs font-mono"></tbody>
</table>
</div>
</div>
</div>
<div id="right-log-card" class="bg-white dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-800 shadow-2xs flex flex-col min-h-0">
<div class="flex items-center justify-between mb-2 shrink-0">
<h3 class="text-xs font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center">
<i class="fa-solid fa-terminal mr-1.5 text-indigo-500"></i> 事件与诊断日志
</h3>
<button onclick="clearLogs()" class="text-[10px] text-slate-400 hover:text-slate-600 transition-colors cursor-pointer">清空</button>
</div>
<div id="terminal-log" class="bg-slate-950 border border-slate-900 rounded p-3 text-[11px] font-mono text-slate-400 flex-1 overflow-y-auto space-y-1.5 shadow-inner custom-scrollbar">
<div class="text-slate-600">[内核层] 初始化高级视口与多线程异步引擎就绪。</div>
</div>
</div>
</div>
</main>
<footer class="text-center py-2 text-[11px] text-slate-400 dark:text-slate-600 border-t border-slate-200 dark:border-slate-900 shrink-0 bg-white dark:bg-slate-900/50" id="footer-app-name">
PortFlow 进程管理器
</footer>
<script>
const invoke = window.__TAURI__ ? (window.__TAURI__.core ? window.__TAURI__.core.invoke : window.__TAURI__.invoke) : null;
let appWindow = null;
if (window.__TAURI__ && window.__TAURI__.window) {
appWindow = window.__TAURI__.window.getCurrentWindow();
}
let allPortsData = [];
let killedCount = 0;
let isInitialLoad = true; // 💡 控制全屏 Loading 仅执行一次
const stateTranslation = {
'LISTENING': '监听中 (Listening)',
'ESTABLISHED': '已建立连接',
'CLOSE_WAIT': '等待关闭',
'TIME_WAIT': '等待被动关闭',
'SYN_SENT': '正在请求连接',
'SYN_RECEIVED': '已收到连接请求',
'FIN_WAIT_1': '等待远端确认关闭',
'FIN_WAIT_2': '等待远端关闭请求',
'LAST_ACK': '等待最后确认',
'CLOSING': '正在关闭',
'UNKNOWN': '未知状态'
};
function getChineseState(stateStr) {
if (!stateStr) return '活动中';
const upperState = stateStr.toUpperCase();
return stateTranslation[upperState] || stateStr;
}
// 同步配置元数据
async function syncAppMetadata() {
if (window.__TAURI__) {
try {
const version = await window.__TAURI__.core.invoke("plugin:app|version");
const name = await window.__TAURI__.core.invoke("plugin:app|name");
document.getElementById('app-version-badge').innerHTML = `<span class="w-1.5 h-1.5 bg-indigo-500 rounded-full mr-2 animate-pulse"></span>版本: V${version}`;
document.getElementById('splash-app-name').innerText = name;
document.getElementById('titlebar-app-name').innerText = `${name} 进程管理器`;
document.getElementById('header-app-name').innerText = `${name} 进程管理器`;
document.getElementById('footer-app-name').innerText = `${name} 进程管理器`;
} catch (e) {
console.error("元数据同步权限受限:", e);
document.getElementById('app-version-badge').innerText = "版本: 内置环境";
}
} else {
document.getElementById('app-version-badge').innerText = "版本: 演示模式";
}
}
function initWindowDrag() {
const titlebar = document.getElementById('custom-titlebar');
const splash = document.getElementById('splash-screen');
const attachDrag = (element) => {
if (element && appWindow) {
element.addEventListener('mousedown', (e) => {
if (!e.target.closest('button')) {
appWindow.startDragging();
}
});
}
};
attachDrag(titlebar);
attachDrag(splash);
}
async function windowMinimize() { if (appWindow) await appWindow.minimize(); }
async function windowMaximize() {
if (appWindow) {
try {
const isMax = await appWindow.isMaximized();
if (isMax) {
await appWindow.unmaximize();
document.getElementById('max-icon').className = 'fa-regular fa-square';
} else {
await appWindow.maximize();
document.getElementById('max-icon').className = 'fa-regular fa-clone';
}
} catch (err) { console.error(err); }
}
}
async function windowClose() { if (appWindow) await appWindow.close(); }
function matchHeight() {
const leftPanel = document.getElementById('left-panel');
const rightCard = document.getElementById('right-log-card');
if (window.innerWidth >= 1024) {
const leftHeight = leftPanel.offsetHeight;
if (leftHeight > 0) rightCard.style.height = `${leftHeight}px`;
} else { rightCard.style.height = 'auto'; }
}
function toggleTheme() {
const isDark = document.documentElement.classList.toggle('dark');
document.getElementById('theme-icon').className = isDark ? 'fa-solid fa-moon text-indigo-400' : 'fa-solid fa-sun text-amber-500';
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
document.getElementById('theme-icon').className = 'fa-solid fa-moon text-indigo-400';
}
function logToTerminal(message, type = 'info') {
const container = document.getElementById('terminal-log');
const time = new Date().toLocaleTimeString();
let colorClass = 'text-slate-400';
if (type === 'success') colorClass = 'text-emerald-400';
if (type === 'error') colorClass = 'text-rose-400';
if (type === 'cmd') colorClass = 'text-indigo-400 font-bold';
container.innerHTML += `<div class="${colorClass}">[${time}] ${message}</div>`;
container.scrollTop = container.scrollHeight;
}
function clearLogs() { document.getElementById('terminal-log').innerHTML = ''; }
function getProcessIcon(name) {
if (!name) return '<i class="fa-solid fa-circle-question text-slate-400 mr-2 text-xs w-4 text-center"></i>';
const lower = name.toLowerCase();
if (lower.includes('node')) return '<i class="fa-brands fa-node-js text-emerald-500 mr-2 text-sm w-4 text-center"></i>';
if (lower.includes('java')) return '<i class="fa-brands fa-java text-orange-500 mr-2 text-sm w-4 text-center"></i>';
if (lower.includes('python')) return '<i class="fa-brands fa-python text-blue-400 mr-2 text-sm w-4 text-center"></i>';
if (lower.includes('nginx') || lower.includes('apache')) return '<i class="fa-solid fa-server text-teal-500 mr-2 text-xs w-4 text-center"></i>';
if (lower.includes('chrome') || lower.includes('edge') || lower.includes('browser')) return '<i class="fa-brands fa-chrome text-amber-500 mr-2 text-xs w-4 text-center"></i>';
if (lower.includes('unknown') || lower.includes('已结束')) return '<i class="fa-solid fa-circle-question text-slate-400 mr-2 text-xs w-4 text-center"></i>';
return '<i class="fa-solid fa-window-maximize text-indigo-400/80 mr-2 text-xs w-4 text-center"></i>';
}
// 💡 优化:带精准区分首次与二次反馈机制的加载函数
async function loadRealPorts() {
logToTerminal(`$ sys_diagnostic --get-network-mapping`, 'cmd');
const refreshIcon = document.getElementById('refresh-icon');
if (isInitialLoad) {
const splash = document.getElementById('splash-screen');
if (splash) {
splash.style.display = 'flex';
splash.classList.remove('opacity-0', 'pointer-events-none');
}
} else if (refreshIcon) {
refreshIcon.classList.add('animate-spin'); // 仅让按钮旋转
}
if (!invoke) {
logToTerminal(`未检测到原生外壳,载入任务管理器级模拟排布。`, 'error');
allPortsData = [
{ protocol: 'TCP', port: 8080, name: 'nginx.exe', pid: 4120, state: 'LISTENING' },
{ protocol: 'TCP', port: 3000, name: 'node.exe', pid: 10424, state: 'LISTENING' },
{ protocol: 'TCP', port: 9000, name: 'java.exe', pid: 8840, state: 'CLOSE_WAIT' },
{ protocol: 'UDP', port: 5353, name: 'chrome.exe', pid: 1402, state: 'ESTABLISHED' },
{ protocol: 'TCP', port: 5432, name: 'postgres.exe', pid: 2112, state: 'LISTENING' }
];
setTimeout(() => {
renderTable(allPortsData);
if (isInitialLoad) { hideSplashScreen(); isInitialLoad = false; }
else if (refreshIcon) refreshIcon.classList.remove('animate-spin');
}, 800);
return;
}
try {
// 执行已经解耦、不会产生界面锁死假死的异步命令
allPortsData = await invoke('get_active_ports');
renderTable(allPortsData);
logToTerminal(`核心映射成功:已提取出 ${allPortsData.length} 个物理网络映射映像。`, 'success');
if (isInitialLoad) { hideSplashScreen(); isInitialLoad = false; }
else if (refreshIcon) refreshIcon.classList.remove('animate-spin');
} catch (err) {
logToTerminal(`提取进程映像失败: ${err}`, 'error');
if (isInitialLoad) {
document.getElementById('splash-status').innerText = "加载失败,请确保使用管理员身份运行: " + err;
} else if (refreshIcon) refreshIcon.classList.remove('animate-spin');
}
}
function hideSplashScreen() {
const splash = document.getElementById('splash-screen');
if (splash) {
splash.classList.add('opacity-0', 'pointer-events-none');
setTimeout(() => { splash.style.display = 'none'; }, 500);
}
}
function filterPorts() {
const query = document.getElementById('search-input').value.toLowerCase().trim();
if (!query) { renderTable(allPortsData); return; }
const filtered = allPortsData.filter(item =>
item.port.toString().includes(query) ||
item.name.toLowerCase().includes(query) ||
item.pid.toString().includes(query) ||
getChineseState(item.state).includes(query)
);
renderTable(filtered);
}
function debounce(fn, delay) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => { fn.apply(this, args); }, delay);
};
}
function throttle(fn, limit) {
let lastFunc = null, lastRan = null;
return function (...args) {
const context = this;
if (!lastRan) { fn.apply(context, args); lastRan = Date.now(); }
else {
if (lastFunc) clearTimeout(lastFunc);
lastFunc = setTimeout(function () {
if ((Date.now() - lastRan) >= limit) { fn.apply(context, args); lastRan = Date.now(); }
}, limit - (Date.now() - lastRan));
}
};
}
const debouncedFilter = debounce(filterPorts, 200);
const throttledLoadPorts = throttle(() => { loadRealPorts(); }, 1500);
function renderTable(ports) {
const tbody = document.getElementById('port-table-body');
document.getElementById('total-ports').innerText = allPortsData.length;
document.getElementById('tcp-count').innerText = allPortsData.filter(p=>p.protocol==='TCP').length;
document.getElementById('udp-count').innerText = allPortsData.filter(p=>p.protocol==='UDP').length;
tbody.innerHTML = '';
if (ports.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" class="text-center py-8 text-slate-400 italic">没有匹配的系统映像活动条目</td></tr>`;
matchHeight();
return;
}
ports.forEach(item => {
const isTcp = item.protocol === 'TCP';
const typeBadge = isTcp ? '<span class="text-blue-600 bg-blue-500/10 border border-blue-500/20 px-1.5 rounded text-[10px]">TCP</span>' : '<span class="text-purple-600 bg-purple-500/10 border border-purple-500/20 px-1.5 rounded text-[10px]">UDP</span>';
const cnState = getChineseState(item.state);
const itemName = item.name || "未知映像";
tbody.innerHTML += `
<tr class="hover:bg-slate-100 dark:hover:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800/40 transition-colors">
<td class="py-2 px-4 font-semibold">${typeBadge}</td>
<td class="py-2 px-4 text-indigo-600 dark:text-indigo-400 font-bold text-sm">:${item.port}</td>
<td class="py-2 px-4 text-slate-700 dark:text-slate-200 flex items-center overflow-hidden text-ellipsis whitespace-nowrap">
${getProcessIcon(itemName)}
<span class="truncate" title="${itemName}">${itemName}</span>
</td>
<td class="py-2 px-4 text-slate-500 dark:text-slate-400">${item.pid}</td>
<td class="py-2 px-4"><span class="text-[11px] text-emerald-600 font-medium whitespace-nowrap"><span class="w-1 h-1 inline-block bg-emerald-500 rounded-full mr-1.5 mb-0.5"></span>${cnState}</span></td>
<td class="py-2 px-4 text-center whitespace-nowrap"><button onclick="killRealProcess(${item.pid}, '${itemName}')" class="bg-rose-600 hover:bg-rose-700 text-white font-sans px-2.5 py-0.5 rounded text-[11px] transition-all cursor-pointer shadow-3xs whitespace-nowrap inline-block">结束任务</button></td>
</tr>`;
});
matchHeight();
}
async function killRealProcess(pid, name) {
if (confirm(`【Windows 任务管理器核心警告】\n\n确定要强行终止映像进程 "${name}" (PID: ${pid}) 吗?\n结束该进程会导致关联的物理端口立即释放,未保存的更改会直接丢失。`)) {
logToTerminal(`$ taskkill /F /PID ${pid}`, 'cmd');
if (!invoke) {
allPortsData = allPortsData.filter(p => p.pid !== pid); renderTable(allPortsData); return;
}
try {
const successMsg = await invoke('kill_process_by_pid', { pid: pid });
logToTerminal(successMsg, 'success');
killedCount++; document.getElementById('killed-count').innerText = killedCount;
await loadRealPorts();
document.getElementById('search-input').value = '';
} catch (err) {
logToTerminal(`进程销毁遭遇拒绝: ${err}`, 'error');
alert(`拒绝访问:强杀此系统核心或高权限进程需要以管理员权限重新启动软件。\n\n${err}`);
}
}
}
window.addEventListener('resize', matchHeight);
window.onload = async () => {
initWindowDrag();
await syncAppMetadata();
await loadRealPorts();
document.getElementById('search-input').addEventListener('input', debouncedFilter);
setTimeout(matchHeight, 150);
};
</script>
</body>
</html>