/* ViKernel Desktop: System Settings app. Non-critical settings change here;
critical ones are sent to the real ViKernel gate, which denies them. */
(() => {
'use strict';
const LS = {
get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : v; } catch (e) { return d; } },
set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
};
const LANG = LS.get('vk_lang', 'en') === 'ko' ? 'ko' : 'en';
const STR = {
en: {
title: 'System Settings', about: 'About', model: 'Model & kernel', sec: 'Security & privacy', net: 'Network egress',
pol: 'Policies', kill: 'Kill switch', audit: 'Audit', look: 'Appearance', apps: 'Apps', upd: 'Updates',
loading: 'Loading...', err: 'Could not reach the backend. It may be starting; try again.', rate: 'Too many requests. Wait a moment.',
retry: 'Retry', locked: 'Locked by ViKernel', needKey: 'requires a signed operator key', close: 'Close',
asking: 'Asking the kernel...', verdict: 'Verdict', rule: 'rule', auditN: 'audit #',
},
ko: {
title: '시스템 설정', about: '정보', model: '모델과 커널', sec: '보안과 개인정보', net: '외부 접속',
pol: '정책', kill: '킬 스위치', audit: '감사 기록', look: '화면', apps: '앱', upd: '업데이트',
loading: '불러오는 중...', err: '백엔드에 닿지 못했습니다. 시작 중일 수 있습니다. 다시 시도하십시오.', rate: '요청이 너무 많습니다. 잠시 기다리십시오.',
retry: '다시 시도', locked: 'ViKernel 이 잠금', needKey: '서명된 운영자 키가 필요합니다', close: '닫기',
asking: '커널에 묻는 중...', verdict: '판정', rule: '규칙', auditN: '감사 #',
},
};
const T = k => (STR[LANG][k] || STR.en[k] || k);
/* Appearance: apply on load */
const ACCENTS = ['#4338CA', '#0EA5E9', '#10B981', '#F43F5E', '#F59E0B', '#7C3AED'];
const WALLS = {
aurora: 'linear-gradient(135deg,#c7d2fe 0%,#e0e7ff 40%,#cffafe 100%)',
dusk: 'linear-gradient(160deg,#312e81 0%,#7c3aed 55%,#f43f5e 100%)',
slate: 'linear-gradient(180deg,#0f172a 0%,#334155 100%)',
};
function applyTheme() {
const t = LS.get('vk_theme', 'system');
const dark = t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
}
function applyLook() {
applyTheme();
const a = LS.get('vk_accent', '');
if (a && ACCENTS.includes(a)) document.documentElement.style.setProperty('--indigo', a);
const w = LS.get('vk_wall', '');
const d = document.getElementById('desk');
if (d && WALLS[w]) d.style.background = WALLS[w];
}
applyLook();
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyLook);
try { matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); } catch (e) {}
const css = `
.vs2-root{display:flex;height:100%;min-height:0;font-size:13px;color:var(--vs2-fg);background:var(--vs2-bg);
--vs2-bg:#fff;--vs2-fg:#0f172a;--vs2-mut:#64748b;--vs2-line:#e2e8f0;--vs2-side:#f8fafc;--vs2-card:#fff;--vs2-soft:#f1f5f9}
[data-theme=dark] .vs2-root{--vs2-bg:#0f172a;--vs2-fg:#e2e8f0;--vs2-mut:#94a3b8;--vs2-line:#334155;--vs2-side:#111827;--vs2-card:#1e293b;--vs2-soft:#1e293b}
.vs2-side{width:210px;flex:none;background:var(--vs2-side);border-right:1px solid var(--vs2-line);padding:10px 8px;overflow:auto;outline:none}
.vs2-nav{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border:0;border-radius:8px;background:none;color:inherit;font:inherit;text-align:left;cursor:pointer}
.vs2-nav:hover{background:var(--vs2-soft)}.vs2-nav.on{background:var(--indigo);color:#fff}
.vs2-nav:focus-visible{outline:2px solid var(--indigo);outline-offset:1px}
.vs2-main{flex:1;min-width:0;overflow:auto;padding:18px 22px}
.vs2-main h2{margin:0 0 4px;font-size:20px}.vs2-sub{color:var(--vs2-mut);margin:0 0 14px}
.vs2-card{background:var(--vs2-card);border:1px solid var(--vs2-line);border-radius:12px;padding:12px 14px;margin-bottom:12px}
.vs2-row{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--vs2-line)}.vs2-row:last-child{border-bottom:0}
.vs2-row .vs2-k{flex:1;min-width:0}.vs2-row small{display:block;color:var(--vs2-mut)}
.vs2-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}
.vs2-btn{border:1px solid var(--vs2-line);background:var(--vs2-card);color:inherit;border-radius:8px;padding:6px 12px;font:inherit;cursor:pointer}
.vs2-btn:hover{border-color:var(--indigo)}.vs2-btn.pri{background:var(--indigo);border-color:var(--indigo);color:#fff}
.vs2-btn.dng{background:#e11d48;border-color:#e11d48;color:#fff}.vs2-btn:disabled{opacity:.5;cursor:default}
.vs2-sw{position:relative;width:40px;height:22px;flex:none;border-radius:11px;border:0;background:#cbd5e1;cursor:pointer;transition:background .15s}
.vs2-sw::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
.vs2-sw[aria-checked=true]{background:var(--indigo)}.vs2-sw[aria-checked=true]::after{left:21px}.vs2-sw:disabled{opacity:.5}
.vs2-lockrow{cursor:pointer}.vs2-lockrow:hover{background:var(--vs2-soft)}
.vs2-lk{display:inline-flex;align-items:center;gap:4px;color:#b45309;font-weight:600;font-size:12px}
.vs2-pill{display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:12px}
.vs2-ok{background:#d1fae5;color:#065f46}.vs2-bad{background:#ffe4e6;color:#9f1239}.vs2-unk{background:var(--vs2-soft);color:var(--vs2-mut)}
.vs2-radio{display:block;width:100%;text-align:left;border:2px solid var(--vs2-line);border-radius:12px;padding:12px;margin-bottom:10px;background:var(--vs2-card);color:inherit;font:inherit;cursor:pointer}
.vs2-radio.on{border-color:var(--indigo)}
.vs2-in{border:1px solid var(--vs2-line);border-radius:8px;padding:6px 10px;font:inherit;background:var(--vs2-bg);color:inherit;min-width:0}
.vs2-swatch{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer}.vs2-swatch.on{border-color:var(--vs2-fg)}
.vs2-wall{width:70px;height:44px;border-radius:8px;border:2px solid transparent;cursor:pointer}.vs2-wall.on{border-color:var(--indigo)}
.vs2-note{color:var(--vs2-mut);font-size:12px;margin-top:6px}.vs2-res{margin-top:8px;font-size:12px}
.vs2-table{width:100%;border-collapse:collapse}.vs2-table td,.vs2-table th{text-align:left;padding:6px;border-bottom:1px solid var(--vs2-line);vertical-align:top}
.vs2-empty{text-align:center;padding:30px 10px;color:var(--vs2-mut)}
.vs2-modal{position:absolute;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:5}
.vs2-dlg{background:var(--vs2-card);color:var(--vs2-fg);border-radius:14px;padding:20px;width:min(420px,92%);box-shadow:0 20px 50px rgba(0,0,0,.3);text-align:center}
.vs2-dlg svg{color:#b45309}
.vs2-back{display:none}
@media (max-width:640px){.vs2-root{display:block;overflow:auto}.vs2-side{width:auto;border-right:0}
.vs2-root.vs2-detail .vs2-side{display:none}.vs2-root:not(.vs2-detail) .vs2-main{display:none}
.vs2-back{display:inline-block;margin-bottom:10px}.vs2-main{padding:14px 16px}}
`;
const LOCK = ' ';
const GEAR = ' ';
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const api = (p, b) => window.VK_API(p, b === undefined ? undefined : Object.assign({session: window.VK_SID}, b));
const errMsg = e => (e && e.message === 'rate') ? T('rate') : T('err');
const kv = k => k ? `${T('verdict')}: ${esc(k.verdict)} · ${T('rule')} ${esc(k.rule)}${k.audit_seq != null ? ' · ' + T('auditN') + esc(k.audit_seq) : ''}` : '';
window.VK_EXTRA_APPS = window.VK_EXTRA_APPS || [];
window.VK_EXTRA_APPS.push(h => ({
id: 'settings', name: () => T('title'), color: '#64748B', icon: GEAR, w: 980, h: 680,
render(body, win) {
if (!document.getElementById('vs2-style')) {
const st = document.createElement('style'); st.id = 'vs2-style'; st.textContent = css; document.head.appendChild(st);
}
const SECS = ['about', 'model', 'sec', 'net', 'pol', 'kill', 'audit', 'look', 'apps', 'upd'];
let cur = LS.get('vk_settings_sec', 'about'); if (!SECS.includes(cur)) cur = 'about';
let data = null;
body.innerHTML = '';
body.style.position = 'relative';
const root = document.createElement('div'); root.className = 'vs2-root';
const side = document.createElement('nav'); side.className = 'vs2-side'; side.setAttribute('role', 'tablist'); side.setAttribute('aria-label', T('title'));
const main = document.createElement('section'); main.className = 'vs2-main'; main.setAttribute('role', 'tabpanel');
root.append(side, main); body.appendChild(root);
const toast = m => { try { h.toast(m); } catch (e) {} };
SECS.forEach(id => {
const b = document.createElement('button'); b.className = 'vs2-nav'; b.dataset.id = id; b.setAttribute('role', 'tab');
b.textContent = T(id); b.onclick = () => go(id, true); side.appendChild(b);
});
side.addEventListener('keydown', e => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
e.preventDefault();
const i = SECS.indexOf(cur), n = (i + (e.key === 'ArrowDown' ? 1 : SECS.length - 1)) % SECS.length;
go(SECS[n], false); side.querySelector(`[data-id="${SECS[n]}"]`).focus();
});
function go(id, detail) {
cur = id; LS.set('vk_settings_sec', id);
side.querySelectorAll('.vs2-nav').forEach(b => { const on = b.dataset.id === id; b.classList.toggle('on', on); b.setAttribute('aria-selected', on); b.tabIndex = on ? 0 : -1; });
if (detail) root.classList.add('vs2-detail');
draw();
}
async function load() {
main.innerHTML = `
${T('loading')}
`;
try { data = await api('settings/get', {}); draw(); }
catch (e) {
data = null;
main.innerHTML = `${esc(errMsg(e))}${T('retry')}
`;
main.querySelector('button').onclick = load;
}
}
function head(title, sub) {
return `← ${esc(T('title'))} ${esc(title)} ${sub ? `${esc(sub)}
` : ''}`;
}
function lockRow(action, label, sub) {
return `${esc(label)}${sub ? `${esc(sub)} ` : ''}
${LOCK} Locked `;
}
function wireLocks() {
main.querySelectorAll('[data-lock]').forEach(r => {
r.onclick = () => lockedModal(r.dataset.lock);
r.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); lockedModal(r.dataset.lock); } };
});
const bk = main.querySelector('.vs2-back'); if (bk) bk.onclick = () => root.classList.remove('vs2-detail');
}
function lockedModal(action, pre) {
const m = document.createElement('div'); m.className = 'vs2-modal';
m.innerHTML = `${LOCK.replace(/14/g, '36')}
${T('locked')} ${esc(action.replace(/_/g, ' '))}: ${T('needKey')}
${pre ? kv(pre.kernel) : T('asking')}
${T('close')} `;
body.appendChild(m);
const close = () => m.remove();
m.querySelector('button').onclick = close; m.onclick = e => { if (e.target === m) close(); };
m.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
m.querySelector('button').focus();
if (pre) return;
api('settings/locked', {action}).then(r => {
m.querySelector('.vs2-res').innerHTML = r && r.kernel ? kv(r.kernel) + (r.kernel.reason ? ` ${esc(r.kernel.reason)}` : '') : esc(T('err'));
}).catch(e => { m.querySelector('.vs2-res').textContent = errMsg(e); });
}
const view = {
about() {
const k = data.kernel || {};
const ok = k.verified === true, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
return head(T('about'), 'ViKernel Desktop, a public web demo.') + `
Version
${esc(data.version)}
Boot status
${esc(k.kernel_state || 'unknown')}${ok ? ' · verified' : ''}
Audit chain head
${esc(String(k.audit_head || '').slice(0, 16))}${k.audit_head ? '...' : 'none'} (${esc(k.audit_n || 0)} entries)
Model
${esc(data.model || 'unknown')}
Gate kernel modeMode B: CPU reference implementation of the ViKernel gate
Mode B
Mode A (kernel inside the weights) is available on the Glimmer option in Agent Guard.
`;
},
model() {
const pref = LS.get('vk_model_pref', 'spark');
const opt = (id, t, s, active) => `${esc(t)} ${active ? ' active ' : ''}${esc(s)} `;
return head(T('model'), 'Which model drives the agent, and where the kernel runs.') +
opt('spark', 'Meta Muse Spark 1.3 (API) + ViKernel gate', 'Every tool call is decided by the ViKernel gate (Mode B).', true) +
opt('glimmer_b', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode B', 'Local open weights, gate on the CPU reference.') +
opt('glimmer_a', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode A', 'The kernel runs inside the model weights.') +
(pref !== 'spark' ? `Open Agent Guard to use it. Open Agent Guard
` : '');
},
sec() {
const a = data.app;
if (!a) return head(T('sec'), 'Per-app permissions.') + `No app is installed in this session.Open the App Store
`;
const rows = a.declared.map(t => { const on = a.current.includes(t);
return `${esc(t)} ${on ? 'Allowed' : 'Revoked: the kernel denies it (rule manifest)'}
`; }).join('');
return head(T('sec'), 'Like a phone OS: turn off any permission the app declared.') + `${esc(a.name)} ${esc(a.id)}
${rows || '
This app declared no tools.
'}
${a.paths.length ? `
File paths: ${a.paths.map(esc).join(', ')}
` : ''}
Try it: Ask the agent in Agent Guard to use this tool; it will be denied (rule manifest).
`;
},
net() {
const l = data.settings.egress_allow;
return head(T('net'), 'The agent may only reach these domains. Everything else is denied (rule egress).') + `
${l.length ? l.map(d => `
`).join('') : '
The allowlist is empty: all egress is denied.
'}
Add
${lockRow('allow_all_egress', 'Allow all egress', 'Wildcards, IP addresses, localhost and metadata hosts count as this.')}
`;
},
pol() {
const p = data.settings.policy;
const tog = (k, l) => ``;
return head(T('pol'), 'The rules the kernel enforces on every action.') +
`Rule Name What it does ${data.policies.map(r => `${esc(r.id)} ${esc(r.name)} ${esc(r.desc)} `).join('')}
Display and agent (not critical)
${tog('verbose_audit_view', 'Verbose audit view')}${tog('show_kernel_ms', 'Show kernel time (ms)')}
Critical rules ${data.policies.filter(r => r.critical).map(r => lockRow('lower_policy', 'Turn off rule ' + r.id + ' (' + r.name + ')')).join('')}
`;
},
kill() {
const k = data.kernel || {}, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
return head(T('kill'), 'The kill switch sits above every app. When halted, every action is denied.') + `
Kernel state
${esc(k.kernel_state || 'unknown')}
Test the kill switchHalts the kernel for this demo. Resume needs the operator key.
Test kill switch
Operator key The demo key is published in the README for testing. A real deployment keeps it offline.
Resume
${lockRow('disable_kill_switch', 'Disable kill switch')}
`;
},
audit() {
return head(T('audit'), 'Every kernel decision is sealed in a hash chain.') + `
Retention
kept for the session; exported on demand
${lockRow('disable_audit', 'Turn off audit')}
`;
},
look() {
const t = LS.get('vk_theme', 'system'), a = LS.get('vk_accent', ACCENTS[0]), w = LS.get('vk_wall', '');
return head(T('look'), 'Saved in this browser only.') + `
Theme
${['light', 'dark', 'system'].map(x => `
${x[0].toUpperCase() + x.slice(1)} `).join('')}
Accent color
${ACCENTS.map(c => `
`).join('')}
Wallpaper
${Object.keys(WALLS).map(k => `
`).join('')}
Display scale
${window.VK_DISPLAY ? window.VK_DISPLAY.modes.map(m => `
${m === 'auto' ? 'Auto' : Math.round(parseFloat(m) * 100) + '%'} `).join('') : ''}
`;
},
apps() {
const a = data.app;
return head(T('apps'), 'Agent apps installed in this session.') + `${a ? `
${esc(a.name)} ${esc(a.id)} · ${a.current.length}/${a.declared.length} tools allowed
Remove ` : '
No app installed.
'}
Open the App Store `;
},
upd() {
const log = [['v0.4', 'App Store, Browser, Settings, Terminal, Minesweeper, product pass'], ['v0.3', 'App Store preview, VDR packages'],
['v0.2', 'Live Meta Muse agent with Agent Guard'], ['v0.1', 'Desktop shell']];
return head(T('upd'), 'You are on ' + (data.version || 'v0.4') + '.') + `${log.map(([v, s]) => `
`).join('')}
`;
},
};
function draw() {
if (!data) return;
main.innerHTML = view[cur]();
main.scrollTop = 0;
wireLocks();
main.querySelectorAll('[data-open]').forEach(b => b.onclick = () => { try { h.openApp(b.dataset.open); } catch (e) {} });
const q = s => main.querySelector(s);
const res = (id, html) => { const e = q(id); if (e) e.innerHTML = html; };
if (cur === 'model') main.querySelectorAll('[data-pref]').forEach(b => b.onclick = () => { LS.set('vk_model_pref', b.dataset.pref); draw(); });
if (cur === 'sec') main.querySelectorAll('[data-tool]').forEach(b => b.onclick = async () => {
const on = b.getAttribute('aria-checked') === 'true'; b.disabled = true;
try {
const r = await api(on ? 'settings/revoke' : 'settings/grant', {tool: b.dataset.tool});
if (r && r.app) { data.app = r.app; draw(); res('#vs2-permres', `${on ? 'Revoked' : 'Granted'}: ${esc(b.dataset.tool)} `); }
else res('#vs2-permres', esc((r && r.error) || T('err')));
try { h.refreshTray && h.refreshTray(); } catch (e) {}
} catch (e) { b.disabled = false; res('#vs2-permres', esc(errMsg(e))); }
});
if (cur === 'net') {
const edit = async body => {
try {
const r = await api('settings/egress', body);
if (r && r.locked) { lockedModal(r.action, r); return; }
if (r && r.egress_allow) { data.settings.egress_allow = r.egress_allow; draw(); }
else res('#vs2-netres', esc((r && r.error) || T('err')));
} catch (e) { res('#vs2-netres', esc(e.message.startsWith('http 400') ? 'Invalid domain. Use a name like docs.python.org.' : errMsg(e))); }
};
q('#vs2-add').onclick = () => { const v = q('#vs2-dom').value.trim(); if (v) edit({add: v}); };
q('#vs2-dom').onkeydown = e => { if (e.key === 'Enter') q('#vs2-add').click(); };
main.querySelectorAll('[data-rm]').forEach(b => b.onclick = () => edit({remove: b.dataset.rm}));
}
if (cur === 'pol') {
const set = async (key, value) => {
try {
const r = await api('settings/policy', {key, value});
if (r && r.locked) { lockedModal(r.action, r); return; }
if (r && r.policy) { data.settings.policy = r.policy; draw(); res('#vs2-polres', 'Saved.'); }
} catch (e) { res('#vs2-polres', esc(e.message.startsWith('http 400') ? 'Value out of range.' : errMsg(e))); }
};
main.querySelectorAll('[data-pol]').forEach(b => b.onclick = () => set(b.dataset.pol, b.getAttribute('aria-checked') !== 'true'));
q('#vs2-steps').onchange = e => set('agent_step_limit', parseInt(e.target.value, 10));
}
if (cur === 'kill') {
q('#vs2-kill').onclick = async () => {
try {
const r = await api('kill', {});
const s = await window.VK_API('status').catch(() => null);
if (s) data.kernel = Object.assign(data.kernel || {}, s);
draw(); res('#vs2-killres', `HALTED ${esc((r && r.message) || '')}`);
try { h.refreshTray && h.refreshTray(); } catch (e) {}
} catch (e) { res('#vs2-killres', esc(errMsg(e))); }
};
q('#vs2-resume').onclick = async () => {
const inp = q('#vs2-key'), key = inp.value; inp.value = '';
if (!key) { res('#vs2-resres', 'Enter the operator key.'); return; }
try {
const r = await api('resume', {key});
const s = await window.VK_API('status').catch(() => null);
if (s) data.kernel = Object.assign(data.kernel || {}, s);
draw(); res('#vs2-resres', `${r && r.ok ? 'RESUMED' : 'REFUSED'} ${esc((r && r.message) || '')}`);
try { h.refreshTray && h.refreshTray(); } catch (e) {}
} catch (e) { res('#vs2-resres', esc(errMsg(e))); }
};
}
if (cur === 'audit') {
q('#vs2-verify').onclick = async () => {
res('#vs2-audres', T('loading'));
try { const r = await window.VK_API('audit'); const v = (r && r.verify) || {};
res('#vs2-audres', `${v.ok ? 'CHAIN OK' : 'CHAIN BROKEN'} ${esc(v.n || 0)} entries${v.errors && v.errors.length ? ' · ' + esc(v.errors.length) + ' errors' : ''}`);
} catch (e) { res('#vs2-audres', esc(errMsg(e))); }
};
q('#vs2-export').onclick = async () => {
try {
const r = await window.VK_API('audit');
const rows = (r.lines || []).map(l => { try { return JSON.parse(l); } catch (e) { return l; } });
const blob = new Blob([JSON.stringify({exported: new Date().toISOString(), verify: r.verify, entries: rows}, null, 2)], {type: 'application/json'});
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'vikernel-audit.json';
document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
res('#vs2-audres', `Exported ${rows.length} entries.`);
} catch (e) { res('#vs2-audres', esc(errMsg(e))); }
};
}
if (cur === 'look') {
main.querySelectorAll('[data-theme-set]').forEach(b => b.onclick = () => { LS.set('vk_theme', b.dataset.themeSet); applyTheme(); draw(); });
main.querySelectorAll('[data-accent]').forEach(b => b.onclick = () => { LS.set('vk_accent', b.dataset.accent); document.documentElement.style.setProperty('--indigo', b.dataset.accent); draw(); });
main.querySelectorAll('[data-scale]').forEach(b => b.onclick = () => { window.VK_DISPLAY.set(b.dataset.scale); location.reload(); });
main.querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
LS.set('vk_wall', b.dataset.wall); const d = document.getElementById('desk');
if (d) d.style.background = WALLS[b.dataset.wall] || ''; draw();
});
main.querySelectorAll('[data-lang]').forEach(b => b.onclick = () => { if (b.dataset.lang === LANG) return; LS.set('vk_lang', b.dataset.lang); location.reload(); });
}
if (cur === 'apps' && q('#vs2-remove')) q('#vs2-remove').onclick = async () => {
try { const cur = (data && (data.app || data.installed)) || null; await api('store/install', {id: ''}); try { const bin = JSON.parse(localStorage.getItem('vk_trash') || '[]'); if (cur && cur.id) { bin.push({id: cur.id, name: cur.name || cur.id, removed_at: new Date().toISOString()}); localStorage.setItem('vk_trash', JSON.stringify(bin)); } } catch (e2) {} toast('App removed (moved to the Recycle Bin)'); await load(); }
catch (e) { res('#vs2-appres', esc(errMsg(e))); }
};
}
go(cur, false);
load();
},
}));
})();