docs: define managed browser exit monitoring
This commit is contained in:
@@ -161,6 +161,8 @@
|
||||
.dialog-inner h2 { margin: 0; font-size: 19px; }
|
||||
.dialog-inner p { margin: 0; color: var(--muted); line-height: 1.55; }
|
||||
.dialog-warning { padding: 11px 12px; border-radius: 9px; background: var(--danger-soft); color: var(--danger); font-size: 12px; line-height: 1.45; }
|
||||
.dialog-list { margin: -3px 0 0; padding-left: 20px; color: var(--text); display: grid; gap: 5px; }
|
||||
.dialog-list li { line-height: 1.45; }
|
||||
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.edit-form { display: grid; gap: 18px; }
|
||||
.edit-form .path-field { grid-template-columns: minmax(0, 1fr) auto; }
|
||||
@@ -218,7 +220,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="prototype-ribbon">PROTOTYPE · 仅供交互确认,非生产实现</div>
|
||||
<div class="prototype-ribbon">PROTOTYPE · 仅供交互确认,非生产实现 · Ctrl+Shift+E 模拟受管浏览器退出</div>
|
||||
<div class="app" id="app">
|
||||
<aside class="sidebar" aria-label="主导航">
|
||||
<div class="brand"><div class="brand-mark" aria-hidden="true">C</div><div><strong>Chub</strong><span>Browser Hub</span></div></div>
|
||||
@@ -277,6 +279,7 @@
|
||||
<dialog class="edit-dialog" id="editDialog" aria-labelledby="editTitle"><form class="dialog-inner edit-form" id="editForm"><div><h2 id="editTitle">编辑实例</h2><p id="editSubtitle">修改下次启动使用的保存配置。</p></div><div class="field"><label for="editName">实例名称 <span>*</span></label><input id="editName" required><small>用于在实例列表中识别此浏览器环境。</small></div><div class="field"><span>浏览器类型</span><div class="radio-row" role="radiogroup" aria-label="浏览器类型"><label><input type="radio" name="editKind" value="chrome" id="editChrome"> Chrome</label><label><input type="radio" name="editKind" value="edge" id="editEdge"> Edge</label></div></div><div class="field"><label for="editDataDir">User Data Dir <span>*</span></label><div class="path-field"><input id="editDataDir" required><button type="button" class="secondary" id="chooseEditDir">选择目录</button></div><small>必须是绝对路径;同一目录不能被多个实例同时使用。</small></div><div class="field"><label for="editURL">启动 URL(可选)</label><input id="editURL" type="url" placeholder="https://example.com"><small>仅支持 http/https;留空时启动浏览器默认页。</small></div><div class="field"><label for="editProxy">代理</label><select id="editProxy"><option value="">无代理</option></select><small>选择已保存的无认证代理;修改后下次启动生效。</small></div><div class="field"><label for="editPort">当前调试端口</label><input class="readonly-value" id="editPort" readonly><small>实际端口由全局起始端口在启动时分配,不能在此固定。</small></div><div class="form-note" id="editReadOnlyNote" hidden>该实例正在运行或为外部关联。为避免配置与运行身份不一致,浏览器类型和 User Data Dir 已锁定。</div><div class="dialog-actions"><button type="button" class="secondary" id="editCancel">取消</button><button type="submit" class="primary" id="editSave">保存更改</button></div></form></dialog>
|
||||
<dialog id="discardDialog" aria-labelledby="discardTitle"><div class="dialog-inner"><h2 id="discardTitle">放弃未保存的更改?</h2><p>可以先保存配置,或放弃本次编辑并关闭对话框。</p><div class="dialog-actions"><button class="secondary" id="keepEditing">继续编辑</button><button class="secondary" id="discardChanges">不保存</button><button class="primary" id="saveChanges">保存</button></div></div></dialog>
|
||||
<dialog id="proxyDeleteDialog" aria-labelledby="proxyDeleteTitle"><div class="dialog-inner"><h2 id="proxyDeleteTitle">删除代理?</h2><p id="proxyDeleteText"></p><div class="dialog-actions"><button class="secondary" id="proxyDeleteCancel">取消</button><button class="danger-button" id="proxyDeleteConfirm">删除代理</button></div></div></dialog>
|
||||
<dialog id="unexpectedExitDialog" aria-labelledby="unexpectedExitTitle" aria-describedby="unexpectedExitText"><div class="dialog-inner"><h2 id="unexpectedExitTitle">浏览器已退出</h2><p id="unexpectedExitText"></p><ul class="dialog-list" id="unexpectedExitList"></ul><div class="form-note">实例配置和 User Data Dir 未被删除。关闭此提示后,可以从实例列表重新启动浏览器。</div><div class="dialog-actions"><button class="primary" id="unexpectedExitAcknowledge">知道了</button></div></div></dialog>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
|
||||
<script>
|
||||
@@ -295,6 +298,8 @@
|
||||
let editingID = '';
|
||||
let editSnapshot = null;
|
||||
let selectedProxyID = '';
|
||||
let unexpectedExitIDs = [];
|
||||
let unexpectedExitFocusID = '';
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const statusClass = (s) => s;
|
||||
const proxyLabel = (id) => proxies.find(p => p.id === id)?.server || '无代理';
|
||||
@@ -352,12 +357,42 @@
|
||||
function hideDrawer() { $('drawer').classList.remove('open'); $('backdrop').classList.remove('open'); $('drawer').setAttribute('aria-hidden', 'true'); }
|
||||
function openConfirm(id, action) { const x = instances.find(i => i.id === id); modalAction = action; $('dialogTitle').textContent = action === 'restart' ? '重启浏览器实例?' : action === 'start' ? '启动浏览器实例?' : '关闭浏览器实例?'; $('dialogText').textContent = action === 'restart' ? `将先请求“${x.name}”正常退出,再使用相同配置重新启动。` : action === 'start' ? `将使用已保存配置启动“${x.name}”。` : `将请求“${x.name}”正常退出。未保存的网页内容由浏览器自行处理。`; $('dialogWarning').hidden = action !== false; $('dialogConfirm').textContent = action === false ? '优雅关闭' : action === 'restart' ? '确认重启' : '启动实例'; $('dialogConfirm').className = action === false ? 'danger-button' : 'primary'; $('confirmDialog').showModal(); }
|
||||
function showToast(message) { const toast = $('toast'); toast.textContent = message; toast.classList.add('show'); clearTimeout(showToast.timer); showToast.timer = setTimeout(() => toast.classList.remove('show'), 3600); }
|
||||
function hasOtherModal() { return [...document.querySelectorAll('dialog[open]')].some(dialog => dialog.id !== 'unexpectedExitDialog'); }
|
||||
function queueUnexpectedExit(id) {
|
||||
const instance = instances.find(x => x.id === id);
|
||||
if (!instance || instance.status !== 'running') return;
|
||||
instance.status = 'exited'; instance.statusText = '已退出'; instance.pid = '—'; instance.port = '—'; instance.exit = '已检测到进程退出';
|
||||
if (!unexpectedExitIDs.includes(id)) unexpectedExitIDs.push(id);
|
||||
selectedId = id;
|
||||
renderRows();
|
||||
presentUnexpectedExitNotice();
|
||||
}
|
||||
function presentUnexpectedExitNotice() {
|
||||
const dialog = $('unexpectedExitDialog');
|
||||
const exited = unexpectedExitIDs.map(id => instances.find(x => x.id === id)).filter(Boolean);
|
||||
if (!exited.length || hasOtherModal()) return;
|
||||
$('unexpectedExitTitle').textContent = exited.length === 1 ? '浏览器已退出' : `${exited.length} 个浏览器已退出`;
|
||||
$('unexpectedExitText').textContent = exited.length === 1 ? `检测到“${exited[0].name}”的 ${exited[0].browser} 已退出。` : '检测到以下由 Chub 启动的浏览器已退出。';
|
||||
$('unexpectedExitList').innerHTML = exited.map(x => `<li><strong>${x.name}</strong> · ${x.browser}</li>`).join('');
|
||||
unexpectedExitFocusID = exited[0].id;
|
||||
if (!dialog.open) { dialog.showModal(); setTimeout(() => $('unexpectedExitAcknowledge').focus(), 0); }
|
||||
}
|
||||
function dismissUnexpectedExitNotice() {
|
||||
const focusID = unexpectedExitFocusID;
|
||||
unexpectedExitIDs = [];
|
||||
$('unexpectedExitDialog').close();
|
||||
requestAnimationFrame(() => {
|
||||
const start = document.querySelector(`tr[data-id="${focusID}"] button[data-action="start"]`);
|
||||
(start || document.querySelector(`tr[data-id="${focusID}"]`))?.focus();
|
||||
});
|
||||
}
|
||||
$('dialogCancel').onclick = () => $('confirmDialog').close(); $('confirmDialog').addEventListener('cancel', e => { e.preventDefault(); $('confirmDialog').close(); }); $('dialogConfirm').onclick = () => { $('confirmDialog').close(); const message = modalAction === false ? '关闭请求已提交,实例将进入 STOPPING。' : modalAction === 'restart' ? '重启请求已提交,正在等待旧实例退出。' : '启动请求已提交,实例将进入 STARTING。'; setTimeout(() => showToast(message), 80); };
|
||||
$('unexpectedExitAcknowledge').onclick = dismissUnexpectedExitNotice; $('unexpectedExitDialog').addEventListener('cancel', e => { e.preventDefault(); dismissUnexpectedExitNotice(); }); document.querySelectorAll('dialog').forEach(dialog => dialog.addEventListener('close', () => setTimeout(presentUnexpectedExitNotice, 0)));
|
||||
function refreshInstances() { const button = $('refresh'); if (button.disabled) return; button.disabled = true; button.textContent = '↻ 刷新中…'; setTimeout(() => { const starting = instances.find(x => x.status === 'starting'); if (starting) { starting.status = 'running'; starting.statusText = '运行中(托管)'; starting.pid = '18468'; starting.port = '9667'; } renderRows(); button.disabled = false; button.textContent = '↻ 刷新状态'; document.querySelector('.table-footer span:last-child').textContent = '最后检查:刚刚'; showToast('已检查保存的实例状态;未扫描其他 Chrome 或 Edge。'); }, 900); }
|
||||
function setPage(page) { const settings = page === 'settings'; $('instancesPage').classList.toggle('page-hidden', settings); $('settingsPage').classList.toggle('page-hidden', !settings); $('pageEyebrow').textContent = settings ? 'Application settings' : 'Browser instances'; $('pageTitle').textContent = settings ? '设置' : '浏览器实例'; $('pageSubtitle').textContent = settings ? '管理浏览器路径、默认目录和本地运行行为' : '启动、观察并安全管理 Chrome 与 Edge 环境'; $('instanceCommands').classList.toggle('page-hidden', settings); if (settings) $('chromePath').focus(); }
|
||||
$('newInstance').onclick = openLaunch; $('backdrop').onclick = hideDrawer; $('search').oninput = renderRows; $('browserFilter').onchange = renderRows; $('statusFilter').onchange = renderRows; $('refresh').onclick = refreshInstances; $('themeToggle').onclick = () => { const dark = document.documentElement.dataset.theme === 'dark'; document.documentElement.dataset.theme = dark ? '' : 'dark'; $('themeToggle').textContent = dark ? '☾' : '☀'; $('themeToggle').setAttribute('aria-label', dark ? '切换深色主题' : '切换浅色主题'); };
|
||||
$('editCancel').onclick = requestEditClose; $('chooseEditDir').onclick = () => showToast('原型演示:将打开目录选择器并回填 User Data Dir。'); $('editForm').addEventListener('submit', e => { e.preventDefault(); saveEdit(); }); $('editDialog').addEventListener('cancel', e => { e.preventDefault(); requestEditClose(); }); $('keepEditing').onclick = () => { $('discardDialog').close(); $('editDialog').showModal(); setTimeout(() => $('editName').focus(), 0); }; $('discardChanges').onclick = () => { $('discardDialog').close(); finishEdit(); }; $('saveChanges').onclick = saveEdit;
|
||||
document.addEventListener('keydown', e => { if (e.key === 'F5' && !$('editDialog').open && !$('discardDialog').open) { e.preventDefault(); refreshInstances(); } });
|
||||
document.addEventListener('keydown', e => { if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'e') { e.preventDefault(); queueUnexpectedExit('inst-chrome-shop'); return; } if (e.key === 'F5' && !$('editDialog').open && !$('discardDialog').open) { e.preventDefault(); refreshInstances(); } });
|
||||
document.querySelectorAll('[data-nav]').forEach(b => b.onclick = () => { document.querySelectorAll('[data-nav]').forEach(n => n.removeAttribute('aria-current')); b.setAttribute('aria-current', 'page'); setPage(b.dataset.nav); });
|
||||
document.querySelectorAll('.browse-setting').forEach(b => b.onclick = () => showToast(`原型演示:将打开${b.dataset.target.includes('Path') ? '文件' : '目录'}选择器。`));
|
||||
document.querySelectorAll('.search-setting').forEach(b => b.onclick = () => { if (b.dataset.timer) { clearTimeout(Number(b.dataset.timer)); delete b.dataset.timer; b.textContent = '搜索'; showToast('已取消当前搜索。'); return; } b.textContent = '取消'; showToast('正在搜索可用路径…'); b.dataset.timer = String(setTimeout(() => { delete b.dataset.timer; b.textContent = '搜索'; showToast('搜索完成:已更新当前字段。'); }, 1600)); });
|
||||
|
||||
Reference in New Issue
Block a user