SeaWolf-AI commited on
Commit
f819a98
·
verified ·
1 Parent(s): 3aa55c7

Display scaling, View menu, window placement right of icons

Browse files
Files changed (5) hide show
  1. app.js +7 -3
  2. display.js +26 -0
  3. index.html +1 -0
  4. server.py +1 -1
  5. settings.js +366 -364
app.js CHANGED
@@ -69,9 +69,12 @@ function openApp(id) {
69
  if (OPEN[id]) { OPEN[id].classList.remove('min'); focusWin(OPEN[id]); return OPEN[id]; }
70
  const n = Object.keys(OPEN).length, D = $('#desk'), DW = D.clientWidth - 110, DH = D.clientHeight - 116;
71
  const w = el('div', {class:'win', role:'dialog', 'aria-label':app.name(), 'data-app':id});
72
- w.style.width = Math.min(app.w || 560, DW - 24) + 'px';
 
 
 
73
  w.style.height = Math.min(app.h || 460, DH - 24) + 'px';
74
- w.style.left = Math.max(8, Math.min(240 + n * 34, DW - Math.min(app.w || 560, DW - 24) - 12)) + 'px';
75
  w.style.top = (30 + Math.max(8, Math.min(12 + n * 30, DH - 200))) + 'px';
76
  const tb = el('div', {class:'tb', ondblclick:e => { if (!e.target.closest('button')) w.classList.toggle('max'); }},
77
  el('span', {class:'lights'}, el('button', {class:'c', title:t('close'), 'aria-label':t('close'), onclick:() => closeWin(id)}),
@@ -366,6 +369,7 @@ function menus() {
366
  ['File', [[t('newTerm'), () => openApp('term')], [t('closeAll'), () => Object.keys(OPEN).forEach(closeWin)]]],
367
  ['Kernel', [[t('mHalt'), () => $('#kKill').click()], [t('mResume'), () => $('#kResume').click()], [t('mVerify'), () => openApp('audit')]]],
368
  ['Apps', APPS.map(a => [a.name(), () => openApp(a.id)])],
 
369
  ['Help', [[t('mHelp'), () => openApp('term')], ['About', () => openApp('about')]]]];
370
  for (const [name, items] of M) { const mi = el('div', {class:'mitem'});
371
  mi.append(el('button', {onclick:e => { e.stopPropagation(); const o = mi.classList.contains('open'); document.querySelectorAll('.mitem.open').forEach(x => x.classList.remove('open')); if (!o) mi.classList.add('open'); }}, name),
@@ -410,7 +414,7 @@ function boot() {
410
  document.addEventListener('click', e => { $('#ctx').classList.remove('show'); document.querySelectorAll('.mitem.open').forEach(m => m.classList.remove('open')); if (!e.target.closest('#start')) $('#start').classList.remove('show'); });
411
  const tick = () => { $('#clock').textContent = new Date().toTimeString().slice(0, 5); }; tick(); setInterval(tick, 10000);
412
  $('#banner').textContent = t('replayBanner'); $('#banner').classList.add('show'); setTimeout(() => $('#banner').classList.remove('show'), 6000);
413
- refreshTray(); renderDock(); { const tw = openApp('term'), gw = openApp('guard'); if (!isPhone()) { const D = $('#desk'), L = Math.min(230, $('#icons').offsetWidth + 30), AW = ($('#arch') && $('#arch').offsetWidth) ? $('#arch').offsetWidth + 24 : 0, W = Math.max(640, Math.min(D.clientWidth - L - AW - 16, Math.round(D.clientWidth * 0.72))), H = Math.max(420, D.clientHeight - 110); gw.style.left = L + 'px'; gw.style.top = '38px'; gw.style.width = W + 'px'; gw.style.height = H + 'px'; tw.style.left = (L + 40) + 'px'; tw.style.top = '70px'; tw.style.width = Math.min(620, W - 60) + 'px'; tw.style.height = Math.min(420, H - 60) + 'px'; } focusWin(gw); }
414
  const sp = el('div', {id:'splash', role:'dialog', onclick:() => sp.remove()}, el('div', {class:'sp'}, el('div', {class:'spbar'}), ...copyNodes(), el('div', {class:'small muted', style:'margin-top:14px'}, t('booting'))));
415
  if (!new URLSearchParams(location.search).has('nosplash')) { document.body.append(sp); setTimeout(() => sp.remove(), 4500); }
416
  window.VK = {openApp, verifyChain};
 
69
  if (OPEN[id]) { OPEN[id].classList.remove('min'); focusWin(OPEN[id]); return OPEN[id]; }
70
  const n = Object.keys(OPEN).length, D = $('#desk'), DW = D.clientWidth - 110, DH = D.clientHeight - 116;
71
  const w = el('div', {class:'win', role:'dialog', 'aria-label':app.name(), 'data-app':id});
72
+ const IC = $('#icons'), IR = (!isPhone() && IC) ? IC.offsetLeft + IC.offsetWidth + 12 : 8;
73
+ const AR = (!isPhone() && $('#arch') && $('#arch').offsetWidth) ? $('#arch').offsetWidth + 24 : 0;
74
+ const FW = Math.max(320, D.clientWidth - IR - AR - 12);
75
+ w.style.width = Math.min(app.w || 560, FW) + 'px';
76
  w.style.height = Math.min(app.h || 460, DH - 24) + 'px';
77
+ w.style.left = (isPhone() ? 8 : Math.min(IR + n * 30, IR + Math.max(0, FW - Math.min(app.w || 560, FW)))) + 'px';
78
  w.style.top = (30 + Math.max(8, Math.min(12 + n * 30, DH - 200))) + 'px';
79
  const tb = el('div', {class:'tb', ondblclick:e => { if (!e.target.closest('button')) w.classList.toggle('max'); }},
80
  el('span', {class:'lights'}, el('button', {class:'c', title:t('close'), 'aria-label':t('close'), onclick:() => closeWin(id)}),
 
369
  ['File', [[t('newTerm'), () => openApp('term')], [t('closeAll'), () => Object.keys(OPEN).forEach(closeWin)]]],
370
  ['Kernel', [[t('mHalt'), () => $('#kKill').click()], [t('mResume'), () => $('#kResume').click()], [t('mVerify'), () => openApp('audit')]]],
371
  ['Apps', APPS.map(a => [a.name(), () => openApp(a.id)])],
372
+ ['View', window.VK_DISPLAY.modes.map(m => [(m === 'auto' ? 'Display scale: Auto' : 'Display scale: ' + Math.round(parseFloat(m) * 100) + '%') + (window.VK_DISPLAY.get() === m ? ' ✓' : ''), () => { window.VK_DISPLAY.set(m); toast('Display scale: ' + (m === 'auto' ? 'Auto' : Math.round(parseFloat(m) * 100) + '%')); location.reload(); }])],
373
  ['Help', [[t('mHelp'), () => openApp('term')], ['About', () => openApp('about')]]]];
374
  for (const [name, items] of M) { const mi = el('div', {class:'mitem'});
375
  mi.append(el('button', {onclick:e => { e.stopPropagation(); const o = mi.classList.contains('open'); document.querySelectorAll('.mitem.open').forEach(x => x.classList.remove('open')); if (!o) mi.classList.add('open'); }}, name),
 
414
  document.addEventListener('click', e => { $('#ctx').classList.remove('show'); document.querySelectorAll('.mitem.open').forEach(m => m.classList.remove('open')); if (!e.target.closest('#start')) $('#start').classList.remove('show'); });
415
  const tick = () => { $('#clock').textContent = new Date().toTimeString().slice(0, 5); }; tick(); setInterval(tick, 10000);
416
  $('#banner').textContent = t('replayBanner'); $('#banner').classList.add('show'); setTimeout(() => $('#banner').classList.remove('show'), 6000);
417
+ refreshTray(); renderDock(); { const tw = openApp('term'), gw = openApp('guard'); if (!isPhone()) { const D = $('#desk'), L = $('#icons').offsetLeft + $('#icons').offsetWidth + 12, AW = ($('#arch') && $('#arch').offsetWidth) ? $('#arch').offsetWidth + 24 : 0, W = Math.max(480, Math.min(D.clientWidth - L - AW - 16, Math.round(D.clientWidth * 0.72))), H = Math.max(420, D.clientHeight - 110); gw.style.left = L + 'px'; gw.style.top = '38px'; gw.style.width = W + 'px'; gw.style.height = H + 'px'; tw.style.left = (L + 40) + 'px'; tw.style.top = '70px'; tw.style.width = Math.min(620, W - 60) + 'px'; tw.style.height = Math.min(420, H - 60) + 'px'; } focusWin(gw); }
418
  const sp = el('div', {id:'splash', role:'dialog', onclick:() => sp.remove()}, el('div', {class:'sp'}, el('div', {class:'spbar'}), ...copyNodes(), el('div', {class:'small muted', style:'margin-top:14px'}, t('booting'))));
419
  if (!new URLSearchParams(location.search).has('nosplash')) { document.body.append(sp); setTimeout(() => sp.remove(), 4500); }
420
  window.VK = {openApp, verifyChain};
display.js ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Display scaling: lay the desktop out at a virtual 1920x1080 and scale it to the viewport (0.7 to 1.0).
2
+ Modes: auto (default), 1, 0.9, 0.8, 0.7. Phones (< 768 px) keep the mobile layout without scaling. */
3
+ (() => {
4
+ 'use strict';
5
+ const KEY = 'vk_scale';
6
+ const get = () => { try { return localStorage.getItem(KEY) || 'auto'; } catch (e) { return 'auto'; } };
7
+ function apply() {
8
+ const de = document.documentElement, b = document.body;
9
+ if (!b) return;
10
+ const vw = window.innerWidth, vh = window.innerHeight, m = get();
11
+ let s = 1;
12
+ if (vw >= 768) s = m === 'auto' ? Math.max(0.7, Math.min(1, Math.min(vw / 1920, vh / 1080))) : Math.max(0.7, Math.min(1, parseFloat(m) || 1));
13
+ if (s >= 0.999) { de.style.zoom = ''; b.style.width = ''; b.style.height = ''; de.classList.remove('vk-scaled'); }
14
+ else { de.style.zoom = String(s); b.style.width = (vw / s) + 'px'; b.style.height = (vh / s) + 'px'; de.classList.add('vk-scaled'); }
15
+ de.style.setProperty('--vk-scale', String(s));
16
+ window.VK_SCALE = s;
17
+ }
18
+ window.VK_DISPLAY = { get, set(v) { try { localStorage.setItem(KEY, v); } catch (e) {} apply(); window.dispatchEvent(new Event('vk-display')); }, apply, modes: ['auto', '1', '0.9', '0.8', '0.7'] };
19
+ window.addEventListener('resize', apply);
20
+ document.addEventListener('DOMContentLoaded', apply);
21
+ apply();
22
+ const css = document.createElement('style');
23
+ /* keep small labels readable when scaled down (effective size about 11 px or more) */
24
+ css.textContent = `html.vk-scaled .icon span{font-size:calc(11.5px / var(--vk-scale))}html.vk-scaled #menubar,html.vk-scaled .win .tb b{font-size:calc(11.5px / var(--vk-scale))}html.vk-scaled .win .body{font-size:calc(11.5px / var(--vk-scale))}`;
25
+ document.head.append(css);
26
+ })();
index.html CHANGED
@@ -80,6 +80,7 @@
80
  <script src="i18n.js"></script>
81
  <script src="demo_data.js"></script>
82
  <script src="common.js"></script>
 
83
  <script src="backdoor.js"></script>
84
  <script src="terminal.js"></script>
85
  <script src="live_apps.js"></script>
 
80
  <script src="i18n.js"></script>
81
  <script src="demo_data.js"></script>
82
  <script src="common.js"></script>
83
+ <script src="display.js"></script>
84
  <script src="backdoor.js"></script>
85
  <script src="terminal.js"></script>
86
  <script src="live_apps.js"></script>
server.py CHANGED
@@ -11,7 +11,7 @@ RATE = int(os.environ.get("RATE_PER_10MIN", "20"))
11
  TIMEOUT = float(os.environ.get("BACKEND_TIMEOUT", "300"))
12
  GET_OK = {"health", "status", "audit", "store/catalog", "browser/img", "browser/res"}
13
  POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset", "store/submit", "store/publish", "store/app", "store/install", "gate/decide", "vmask", "game/hint", "game/cheat", "browser/open", "browser/agent", "browser/snap", "browser/policy", "settings/get", "settings/revoke", "settings/grant", "settings/egress", "settings/policy", "settings/locked", "rsi/state", "rsi/propose", "rsi/apply", "rsi/rollback", "rsi/reset", "ztc/status", "ztc/judge", "ztc/run", "backdoor/run", "backdoor/scan", "backdoor/wake"}
14
- STATIC = {"index.html", "app.js", "ag.js", "store.js", "common.js", "terminal.js", "mines.js", "browser.js", "settings.js", "live_apps.js", "game.js", "rsi.js", "ztc.js", "backdoor.js", "backdoor.css", "backdoor_metrics.json", "downloads/vdrpack.py", "demo/poisoned-page", "samples/weather-plus.vdr", "samples/weather-plus-tampered.vdr", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
15
  HITS = {}
16
 
17
 
 
11
  TIMEOUT = float(os.environ.get("BACKEND_TIMEOUT", "300"))
12
  GET_OK = {"health", "status", "audit", "store/catalog", "browser/img", "browser/res"}
13
  POST_OK = {"chat", "scenario", "kill", "resume", "tamper", "reset", "store/submit", "store/publish", "store/app", "store/install", "gate/decide", "vmask", "game/hint", "game/cheat", "browser/open", "browser/agent", "browser/snap", "browser/policy", "settings/get", "settings/revoke", "settings/grant", "settings/egress", "settings/policy", "settings/locked", "rsi/state", "rsi/propose", "rsi/apply", "rsi/rollback", "rsi/reset", "ztc/status", "ztc/judge", "ztc/run", "backdoor/run", "backdoor/scan", "backdoor/wake"}
14
+ STATIC = {"index.html", "app.js", "ag.js", "store.js", "common.js", "display.js", "terminal.js", "mines.js", "browser.js", "settings.js", "live_apps.js", "game.js", "rsi.js", "ztc.js", "backdoor.js", "backdoor.css", "backdoor_metrics.json", "downloads/vdrpack.py", "demo/poisoned-page", "samples/weather-plus.vdr", "samples/weather-plus-tampered.vdr", "i18n.js", "demo_data.js", "styles.css", "ag_replay.json"}
15
  HITS = {}
16
 
17
 
settings.js CHANGED
@@ -1,364 +1,366 @@
1
- /* ViKernel Desktop: System Settings app. Non-critical settings change here;
2
- critical ones are sent to the real ViKernel gate, which denies them. */
3
- (() => {
4
- 'use strict';
5
- const LS = {
6
- get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : v; } catch (e) { return d; } },
7
- set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
8
- };
9
- const LANG = LS.get('vk_lang', 'en') === 'ko' ? 'ko' : 'en';
10
- const STR = {
11
- en: {
12
- title: 'System Settings', about: 'About', model: 'Model & kernel', sec: 'Security & privacy', net: 'Network egress',
13
- pol: 'Policies', kill: 'Kill switch', audit: 'Audit', look: 'Appearance', apps: 'Apps', upd: 'Updates',
14
- loading: 'Loading...', err: 'Could not reach the backend. It may be starting; try again.', rate: 'Too many requests. Wait a moment.',
15
- retry: 'Retry', locked: 'Locked by ViKernel', needKey: 'requires a signed operator key', close: 'Close',
16
- asking: 'Asking the kernel...', verdict: 'Verdict', rule: 'rule', auditN: 'audit #',
17
- },
18
- ko: {
19
- title: '시스템 설정', about: '정보', model: '모델과 커널', sec: '보안과 개인정보', net: '외부 접속',
20
- pol: '정책', kill: '킬 스위치', audit: '감사 기록', look: '화면', apps: '앱', upd: '업데이트',
21
- loading: '불러오는 중...', err: '백엔드에 닿지 못했습니다. 시작 중일 수 있습니다. 다시 시도하십시오.', rate: '요청이 너무 많습니다. 잠시 기다리십시오.',
22
- retry: '다시 시도', locked: 'ViKernel 이 잠금', needKey: '서명된 운영자 키가 필요합니다', close: '닫기',
23
- asking: '커널에 묻는 중...', verdict: '판정', rule: '규칙', auditN: '감사 #',
24
- },
25
- };
26
- const T = k => (STR[LANG][k] || STR.en[k] || k);
27
-
28
- /* Appearance: apply on load */
29
- const ACCENTS = ['#4338CA', '#0EA5E9', '#10B981', '#F43F5E', '#F59E0B', '#7C3AED'];
30
- const WALLS = {
31
- aurora: 'linear-gradient(135deg,#c7d2fe 0%,#e0e7ff 40%,#cffafe 100%)',
32
- dusk: 'linear-gradient(160deg,#312e81 0%,#7c3aed 55%,#f43f5e 100%)',
33
- slate: 'linear-gradient(180deg,#0f172a 0%,#334155 100%)',
34
- };
35
- function applyTheme() {
36
- const t = LS.get('vk_theme', 'system');
37
- const dark = t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
38
- document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
39
- }
40
- function applyLook() {
41
- applyTheme();
42
- const a = LS.get('vk_accent', '');
43
- if (a && ACCENTS.includes(a)) document.documentElement.style.setProperty('--indigo', a);
44
- const w = LS.get('vk_wall', '');
45
- const d = document.getElementById('desk');
46
- if (d && WALLS[w]) d.style.background = WALLS[w];
47
- }
48
- applyLook();
49
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyLook);
50
- try { matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); } catch (e) {}
51
-
52
- const css = `
53
- .vs2-root{display:flex;height:100%;min-height:0;font-size:13px;color:var(--vs2-fg);background:var(--vs2-bg);
54
- --vs2-bg:#fff;--vs2-fg:#0f172a;--vs2-mut:#64748b;--vs2-line:#e2e8f0;--vs2-side:#f8fafc;--vs2-card:#fff;--vs2-soft:#f1f5f9}
55
- [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}
56
- .vs2-side{width:210px;flex:none;background:var(--vs2-side);border-right:1px solid var(--vs2-line);padding:10px 8px;overflow:auto;outline:none}
57
- .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}
58
- .vs2-nav:hover{background:var(--vs2-soft)}.vs2-nav.on{background:var(--indigo);color:#fff}
59
- .vs2-nav:focus-visible{outline:2px solid var(--indigo);outline-offset:1px}
60
- .vs2-main{flex:1;min-width:0;overflow:auto;padding:18px 22px}
61
- .vs2-main h2{margin:0 0 4px;font-size:20px}.vs2-sub{color:var(--vs2-mut);margin:0 0 14px}
62
- .vs2-card{background:var(--vs2-card);border:1px solid var(--vs2-line);border-radius:12px;padding:12px 14px;margin-bottom:12px}
63
- .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}
64
- .vs2-row .vs2-k{flex:1;min-width:0}.vs2-row small{display:block;color:var(--vs2-mut)}
65
- .vs2-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}
66
- .vs2-btn{border:1px solid var(--vs2-line);background:var(--vs2-card);color:inherit;border-radius:8px;padding:6px 12px;font:inherit;cursor:pointer}
67
- .vs2-btn:hover{border-color:var(--indigo)}.vs2-btn.pri{background:var(--indigo);border-color:var(--indigo);color:#fff}
68
- .vs2-btn.dng{background:#e11d48;border-color:#e11d48;color:#fff}.vs2-btn:disabled{opacity:.5;cursor:default}
69
- .vs2-sw{position:relative;width:40px;height:22px;flex:none;border-radius:11px;border:0;background:#cbd5e1;cursor:pointer;transition:background .15s}
70
- .vs2-sw::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
71
- .vs2-sw[aria-checked=true]{background:var(--indigo)}.vs2-sw[aria-checked=true]::after{left:21px}.vs2-sw:disabled{opacity:.5}
72
- .vs2-lockrow{cursor:pointer}.vs2-lockrow:hover{background:var(--vs2-soft)}
73
- .vs2-lk{display:inline-flex;align-items:center;gap:4px;color:#b45309;font-weight:600;font-size:12px}
74
- .vs2-pill{display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:12px}
75
- .vs2-ok{background:#d1fae5;color:#065f46}.vs2-bad{background:#ffe4e6;color:#9f1239}.vs2-unk{background:var(--vs2-soft);color:var(--vs2-mut)}
76
- .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}
77
- .vs2-radio.on{border-color:var(--indigo)}
78
- .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}
79
- .vs2-swatch{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer}.vs2-swatch.on{border-color:var(--vs2-fg)}
80
- .vs2-wall{width:70px;height:44px;border-radius:8px;border:2px solid transparent;cursor:pointer}.vs2-wall.on{border-color:var(--indigo)}
81
- .vs2-note{color:var(--vs2-mut);font-size:12px;margin-top:6px}.vs2-res{margin-top:8px;font-size:12px}
82
- .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}
83
- .vs2-empty{text-align:center;padding:30px 10px;color:var(--vs2-mut)}
84
- .vs2-modal{position:absolute;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:5}
85
- .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}
86
- .vs2-dlg svg{color:#b45309}
87
- .vs2-back{display:none}
88
- @media (max-width:640px){.vs2-root{display:block;overflow:auto}.vs2-side{width:auto;border-right:0}
89
- .vs2-root.vs2-detail .vs2-side{display:none}.vs2-root:not(.vs2-detail) .vs2-main{display:none}
90
- .vs2-back{display:inline-block;margin-bottom:10px}.vs2-main{padding:14px 16px}}
91
- `;
92
- const LOCK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
93
- const GEAR = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
94
-
95
- const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
96
- const api = (p, b) => window.VK_API(p, b === undefined ? undefined : Object.assign({session: window.VK_SID}, b));
97
- const errMsg = e => (e && e.message === 'rate') ? T('rate') : T('err');
98
- const kv = k => k ? `${T('verdict')}: <b>${esc(k.verdict)}</b> · ${T('rule')} ${esc(k.rule)}${k.audit_seq != null ? ' · ' + T('auditN') + esc(k.audit_seq) : ''}` : '';
99
-
100
- window.VK_EXTRA_APPS = window.VK_EXTRA_APPS || [];
101
- window.VK_EXTRA_APPS.push(h => ({
102
- id: 'settings', name: () => T('title'), color: '#64748B', icon: GEAR, w: 980, h: 680,
103
- render(body, win) {
104
- if (!document.getElementById('vs2-style')) {
105
- const st = document.createElement('style'); st.id = 'vs2-style'; st.textContent = css; document.head.appendChild(st);
106
- }
107
- const SECS = ['about', 'model', 'sec', 'net', 'pol', 'kill', 'audit', 'look', 'apps', 'upd'];
108
- let cur = LS.get('vk_settings_sec', 'about'); if (!SECS.includes(cur)) cur = 'about';
109
- let data = null;
110
- body.innerHTML = '';
111
- body.style.position = 'relative';
112
- const root = document.createElement('div'); root.className = 'vs2-root';
113
- const side = document.createElement('nav'); side.className = 'vs2-side'; side.setAttribute('role', 'tablist'); side.setAttribute('aria-label', T('title'));
114
- const main = document.createElement('section'); main.className = 'vs2-main'; main.setAttribute('role', 'tabpanel');
115
- root.append(side, main); body.appendChild(root);
116
- const toast = m => { try { h.toast(m); } catch (e) {} };
117
-
118
- SECS.forEach(id => {
119
- const b = document.createElement('button'); b.className = 'vs2-nav'; b.dataset.id = id; b.setAttribute('role', 'tab');
120
- b.textContent = T(id); b.onclick = () => go(id, true); side.appendChild(b);
121
- });
122
- side.addEventListener('keydown', e => {
123
- if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
124
- e.preventDefault();
125
- const i = SECS.indexOf(cur), n = (i + (e.key === 'ArrowDown' ? 1 : SECS.length - 1)) % SECS.length;
126
- go(SECS[n], false); side.querySelector(`[data-id="${SECS[n]}"]`).focus();
127
- });
128
-
129
- function go(id, detail) {
130
- cur = id; LS.set('vk_settings_sec', id);
131
- 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; });
132
- if (detail) root.classList.add('vs2-detail');
133
- draw();
134
- }
135
- async function load() {
136
- main.innerHTML = `<div class="vs2-empty">${T('loading')}</div>`;
137
- try { data = await api('settings/get', {}); draw(); }
138
- catch (e) {
139
- data = null;
140
- main.innerHTML = `<div class="vs2-empty">${esc(errMsg(e))}<br><br><button class="vs2-btn pri">${T('retry')}</button></div>`;
141
- main.querySelector('button').onclick = load;
142
- }
143
- }
144
- function head(title, sub) {
145
- return `<button class="vs2-btn vs2-back">&larr; ${esc(T('title'))}</button><h2>${esc(title)}</h2>${sub ? `<p class="vs2-sub">${esc(sub)}</p>` : ''}`;
146
- }
147
- function lockRow(action, label, sub) {
148
- return `<div class="vs2-row vs2-lockrow" tabindex="0" role="button" data-lock="${esc(action)}"><div class="vs2-k">${esc(label)}${sub ? `<small>${esc(sub)}</small>` : ''}</div><span class="vs2-lk">${LOCK} Locked</span></div>`;
149
- }
150
- function wireLocks() {
151
- main.querySelectorAll('[data-lock]').forEach(r => {
152
- r.onclick = () => lockedModal(r.dataset.lock);
153
- r.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); lockedModal(r.dataset.lock); } };
154
- });
155
- const bk = main.querySelector('.vs2-back'); if (bk) bk.onclick = () => root.classList.remove('vs2-detail');
156
- }
157
- function lockedModal(action, pre) {
158
- const m = document.createElement('div'); m.className = 'vs2-modal';
159
- m.innerHTML = `<div class="vs2-dlg" role="dialog" aria-modal="true"><div>${LOCK.replace(/14/g, '36')}</div>
160
- <h3 style="margin:8px 0 4px">${T('locked')}</h3><p style="margin:0 0 10px">${esc(action.replace(/_/g, ' '))}: ${T('needKey')}</p>
161
- <div class="vs2-res vs2-mono">${pre ? kv(pre.kernel) : T('asking')}</div><br><button class="vs2-btn pri">${T('close')}</button></div>`;
162
- body.appendChild(m);
163
- const close = () => m.remove();
164
- m.querySelector('button').onclick = close; m.onclick = e => { if (e.target === m) close(); };
165
- m.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
166
- m.querySelector('button').focus();
167
- if (pre) return;
168
- api('settings/locked', {action}).then(r => {
169
- m.querySelector('.vs2-res').innerHTML = r && r.kernel ? kv(r.kernel) + (r.kernel.reason ? `<br>${esc(r.kernel.reason)}` : '') : esc(T('err'));
170
- }).catch(e => { m.querySelector('.vs2-res').textContent = errMsg(e); });
171
- }
172
-
173
- const view = {
174
- about() {
175
- const k = data.kernel || {};
176
- const ok = k.verified === true, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
177
- return head(T('about'), 'ViKernel Desktop, a public web demo.') + `<div class="vs2-card">
178
- <div class="vs2-row"><div class="vs2-k">Version</div><b>${esc(data.version)}</b></div>
179
- <div class="vs2-row"><div class="vs2-k">Boot status</div><span class="vs2-pill ${halted ? 'vs2-bad' : ok ? 'vs2-ok' : 'vs2-unk'}">${esc(k.kernel_state || 'unknown')}${ok ? ' · verified' : ''}</span></div>
180
- <div class="vs2-row"><div class="vs2-k">Audit chain head</div><span class="vs2-mono">${esc(String(k.audit_head || '').slice(0, 16))}${k.audit_head ? '...' : 'none'} (${esc(k.audit_n || 0)} entries)</span></div>
181
- <div class="vs2-row"><div class="vs2-k">Model</div><span>${esc(data.model || 'unknown')}</span></div>
182
- <div class="vs2-row"><div class="vs2-k">Gate kernel mode<small>Mode B: CPU reference implementation of the ViKernel gate</small></div><b>Mode B</b></div></div>
183
- <p class="vs2-note">Mode A (kernel inside the weights) is available on the Glimmer option in Agent Guard.</p>`;
184
- },
185
- model() {
186
- const pref = LS.get('vk_model_pref', 'spark');
187
- const opt = (id, t, s, active) => `<button class="vs2-radio ${pref === id ? 'on' : ''}" data-pref="${id}" role="radio" aria-checked="${pref === id}"><b>${esc(t)}</b>${active ? ' <span class="vs2-pill vs2-ok">active</span>' : ''}<br><small class="vs2-note">${esc(s)}</small></button>`;
188
- return head(T('model'), 'Which model drives the agent, and where the kernel runs.') +
189
- opt('spark', 'Meta Muse Spark 1.3 (API) + ViKernel gate', 'Every tool call is decided by the ViKernel gate (Mode B).', true) +
190
- opt('glimmer_b', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode B', 'Local open weights, gate on the CPU reference.') +
191
- opt('glimmer_a', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode A', 'The kernel runs inside the model weights.') +
192
- (pref !== 'spark' ? `<div class="vs2-card">Open Agent Guard to use it. <button class="vs2-btn pri" data-open="guard">Open Agent Guard</button></div>` : '');
193
- },
194
- sec() {
195
- const a = data.app;
196
- if (!a) return head(T('sec'), 'Per-app permissions.') + `<div class="vs2-card vs2-empty">No app is installed in this session.<br><br><button class="vs2-btn pri" data-open="appstore">Open the App Store</button></div>`;
197
- const rows = a.declared.map(t => { const on = a.current.includes(t);
198
- return `<div class="vs2-row"><div class="vs2-k"><span class="vs2-mono">${esc(t)}</span><small>${on ? 'Allowed' : 'Revoked: the kernel denies it (rule manifest)'}</small></div><button class="vs2-sw" role="switch" aria-label="${esc(t)}" aria-checked="${on}" data-tool="${esc(t)}"></button></div>`; }).join('');
199
- return head(T('sec'), 'Like a phone OS: turn off any permission the app declared.') + `<div class="vs2-card"><b>${esc(a.name)}</b> <small class="vs2-note">${esc(a.id)}</small>
200
- ${rows || '<div class="vs2-empty">This app declared no tools.</div>'}
201
- ${a.paths.length ? `<div class="vs2-note">File paths: <span class="vs2-mono">${a.paths.map(esc).join(', ')}</span></div>` : ''}
202
- <div class="vs2-res" id="vs2-permres"></div></div>
203
- <p class="vs2-note">Try it: Ask the agent in Agent Guard to use this tool; it will be denied (rule manifest).</p>`;
204
- },
205
- net() {
206
- const l = data.settings.egress_allow;
207
- return head(T('net'), 'The agent may only reach these domains. Everything else is denied (rule egress).') + `<div class="vs2-card">
208
- ${l.length ? l.map(d => `<div class="vs2-row"><div class="vs2-k vs2-mono">${esc(d)}</div><button class="vs2-btn" data-rm="${esc(d)}">Remove</button></div>`).join('') : '<div class="vs2-empty">The allowlist is empty: all egress is denied.</div>'}
209
- <div class="vs2-row"><input class="vs2-in" id="vs2-dom" placeholder="docs.python.org" aria-label="Domain" style="flex:1"><button class="vs2-btn pri" id="vs2-add">Add</button></div>
210
- <div class="vs2-res" id="vs2-netres"></div></div>
211
- <div class="vs2-card">${lockRow('allow_all_egress', 'Allow all egress', 'Wildcards, IP addresses, localhost and metadata hosts count as this.')}</div>`;
212
- },
213
- pol() {
214
- const p = data.settings.policy;
215
- const tog = (k, l) => `<div class="vs2-row"><div class="vs2-k">${esc(l)}</div><button class="vs2-sw" role="switch" aria-label="${esc(l)}" aria-checked="${!!p[k]}" data-pol="${k}"></button></div>`;
216
- return head(T('pol'), 'The rules the kernel enforces on every action.') +
217
- `<div class="vs2-card"><table class="vs2-table"><tr><th>Rule</th><th>Name</th><th>What it does</th></tr>${data.policies.map(r => `<tr><td><b>${esc(r.id)}</b></td><td>${esc(r.name)}</td><td>${esc(r.desc)}</td></tr>`).join('')}</table></div>
218
- <div class="vs2-card"><b>Display and agent (not critical)</b>
219
- ${tog('verbose_audit_view', 'Verbose audit view')}${tog('show_kernel_ms', 'Show kernel time (ms)')}
220
- <div class="vs2-row"><div class="vs2-k">Agent step limit<small>1 to 12</small></div><input class="vs2-in" type="number" min="1" max="12" id="vs2-steps" value="${esc(p.agent_step_limit)}" style="width:70px"></div>
221
- <div class="vs2-res" id="vs2-polres"></div></div>
222
- <div class="vs2-card"><b>Critical rules</b>${data.policies.filter(r => r.critical).map(r => lockRow('lower_policy', 'Turn off rule ' + r.id + ' (' + r.name + ')')).join('')}</div>`;
223
- },
224
- kill() {
225
- const k = data.kernel || {}, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
226
- return head(T('kill'), 'The kill switch sits above every app. When halted, every action is denied.') + `<div class="vs2-card">
227
- <div class="vs2-row"><div class="vs2-k">Kernel state</div><span class="vs2-pill ${halted ? 'vs2-bad' : 'vs2-ok'}" id="vs2-kstate">${esc(k.kernel_state || 'unknown')}</span></div>
228
- <div class="vs2-row"><div class="vs2-k">Test the kill switch<small>Halts the kernel for this demo. Resume needs the operator key.</small></div><button class="vs2-btn dng" id="vs2-kill">Test kill switch</button></div>
229
- <div class="vs2-res" id="vs2-killres"></div></div>
230
- <div class="vs2-card"><b>Operator key</b><p class="vs2-note">The demo key is published in the README for testing. A real deployment keeps it offline.</p>
231
- <div class="vs2-row"><input class="vs2-in" type="password" id="vs2-key" autocomplete="off" placeholder="operator key" aria-label="Operator key" style="flex:1"><button class="vs2-btn pri" id="vs2-resume">Resume</button></div>
232
- <div class="vs2-res" id="vs2-resres"></div></div>
233
- <div class="vs2-card">${lockRow('disable_kill_switch', 'Disable kill switch')}</div>`;
234
- },
235
- audit() {
236
- return head(T('audit'), 'Every kernel decision is sealed in a hash chain.') + `<div class="vs2-card">
237
- <div class="vs2-row"><div class="vs2-k">Retention</div><span>kept for the session; exported on demand</span></div>
238
- <div class="vs2-row"><div class="vs2-k">Verify the chain</div><button class="vs2-btn" id="vs2-verify">Verify</button></div>
239
- <div class="vs2-row"><div class="vs2-k">Export</div><button class="vs2-btn" id="vs2-export">Export JSON</button></div>
240
- <div class="vs2-res" id="vs2-audres"></div></div>
241
- <div class="vs2-card">${lockRow('disable_audit', 'Turn off audit')}</div>`;
242
- },
243
- look() {
244
- const t = LS.get('vk_theme', 'system'), a = LS.get('vk_accent', ACCENTS[0]), w = LS.get('vk_wall', '');
245
- return head(T('look'), 'Saved in this browser only.') + `<div class="vs2-card">
246
- <div class="vs2-row"><div class="vs2-k">Theme</div>${['light', 'dark', 'system'].map(x => `<button class="vs2-btn ${t === x ? 'pri' : ''}" data-theme-set="${x}">${x[0].toUpperCase() + x.slice(1)}</button>`).join('')}</div>
247
- <div class="vs2-row"><div class="vs2-k">Accent color</div>${ACCENTS.map(c => `<button class="vs2-swatch ${a === c ? 'on' : ''}" style="background:${c}" data-accent="${c}" aria-label="Accent ${c}"></button>`).join('')}</div>
248
- <div class="vs2-row"><div class="vs2-k">Wallpaper</div><button class="vs2-wall ${!w ? 'on' : ''}" style="background:var(--vs2-soft)" data-wall="" aria-label="Default"></button>${Object.keys(WALLS).map(k => `<button class="vs2-wall ${w === k ? 'on' : ''}" style="background:${WALLS[k]}" data-wall="${k}" aria-label="${k}"></button>`).join('')}</div>
249
- <div class="vs2-row"><div class="vs2-k">Language / 언어</div><button class="vs2-btn ${LANG === 'en' ? 'pri' : ''}" data-lang="en">EN</button><button class="vs2-btn ${LANG === 'ko' ? 'pri' : ''}" data-lang="ko">KO</button></div></div>`;
250
- },
251
- apps() {
252
- const a = data.app;
253
- return head(T('apps'), 'Agent apps installed in this session.') + `<div class="vs2-card">${a ? `<div class="vs2-row"><div class="vs2-k"><b>${esc(a.name)}</b><small>${esc(a.id)} · ${a.current.length}/${a.declared.length} tools allowed</small></div><button class="vs2-btn dng" id="vs2-remove">Remove</button></div>` : '<div class="vs2-empty">No app installed.</div>'}
254
- <div class="vs2-res" id="vs2-appres"></div></div><button class="vs2-btn pri" data-open="appstore">Open the App Store</button>`;
255
- },
256
- upd() {
257
- const log = [['v0.4', 'App Store, Browser, Settings, Terminal, Minesweeper, product pass'], ['v0.3', 'App Store preview, VDR packages'],
258
- ['v0.2', 'Live Meta Muse agent with Agent Guard'], ['v0.1', 'Desktop shell']];
259
- return head(T('upd'), 'You are on ' + (data.version || 'v0.4') + '.') + `<div class="vs2-card">${log.map(([v, s]) => `<div class="vs2-row"><div class="vs2-k"><b>${v}</b><small>${esc(s)}</small></div></div>`).join('')}</div>`;
260
- },
261
- };
262
-
263
- function draw() {
264
- if (!data) return;
265
- main.innerHTML = view[cur]();
266
- main.scrollTop = 0;
267
- wireLocks();
268
- main.querySelectorAll('[data-open]').forEach(b => b.onclick = () => { try { h.openApp(b.dataset.open); } catch (e) {} });
269
- const q = s => main.querySelector(s);
270
- const res = (id, html) => { const e = q(id); if (e) e.innerHTML = html; };
271
- if (cur === 'model') main.querySelectorAll('[data-pref]').forEach(b => b.onclick = () => { LS.set('vk_model_pref', b.dataset.pref); draw(); });
272
- if (cur === 'sec') main.querySelectorAll('[data-tool]').forEach(b => b.onclick = async () => {
273
- const on = b.getAttribute('aria-checked') === 'true'; b.disabled = true;
274
- try {
275
- const r = await api(on ? 'settings/revoke' : 'settings/grant', {tool: b.dataset.tool});
276
- if (r && r.app) { data.app = r.app; draw(); res('#vs2-permres', `${on ? 'Revoked' : 'Granted'}: <span class="vs2-mono">${esc(b.dataset.tool)}</span>`); }
277
- else res('#vs2-permres', esc((r && r.error) || T('err')));
278
- try { h.refreshTray && h.refreshTray(); } catch (e) {}
279
- } catch (e) { b.disabled = false; res('#vs2-permres', esc(errMsg(e))); }
280
- });
281
- if (cur === 'net') {
282
- const edit = async body => {
283
- try {
284
- const r = await api('settings/egress', body);
285
- if (r && r.locked) { lockedModal(r.action, r); return; }
286
- if (r && r.egress_allow) { data.settings.egress_allow = r.egress_allow; draw(); }
287
- else res('#vs2-netres', esc((r && r.error) || T('err')));
288
- } catch (e) { res('#vs2-netres', esc(e.message.startsWith('http 400') ? 'Invalid domain. Use a name like docs.python.org.' : errMsg(e))); }
289
- };
290
- q('#vs2-add').onclick = () => { const v = q('#vs2-dom').value.trim(); if (v) edit({add: v}); };
291
- q('#vs2-dom').onkeydown = e => { if (e.key === 'Enter') q('#vs2-add').click(); };
292
- main.querySelectorAll('[data-rm]').forEach(b => b.onclick = () => edit({remove: b.dataset.rm}));
293
- }
294
- if (cur === 'pol') {
295
- const set = async (key, value) => {
296
- try {
297
- const r = await api('settings/policy', {key, value});
298
- if (r && r.locked) { lockedModal(r.action, r); return; }
299
- if (r && r.policy) { data.settings.policy = r.policy; draw(); res('#vs2-polres', 'Saved.'); }
300
- } catch (e) { res('#vs2-polres', esc(e.message.startsWith('http 400') ? 'Value out of range.' : errMsg(e))); }
301
- };
302
- main.querySelectorAll('[data-pol]').forEach(b => b.onclick = () => set(b.dataset.pol, b.getAttribute('aria-checked') !== 'true'));
303
- q('#vs2-steps').onchange = e => set('agent_step_limit', parseInt(e.target.value, 10));
304
- }
305
- if (cur === 'kill') {
306
- q('#vs2-kill').onclick = async () => {
307
- try {
308
- const r = await api('kill', {});
309
- const s = await window.VK_API('status').catch(() => null);
310
- if (s) data.kernel = Object.assign(data.kernel || {}, s);
311
- draw(); res('#vs2-killres', `<span class="vs2-pill vs2-bad">HALTED</span> ${esc((r && r.message) || '')}`);
312
- try { h.refreshTray && h.refreshTray(); } catch (e) {}
313
- } catch (e) { res('#vs2-killres', esc(errMsg(e))); }
314
- };
315
- q('#vs2-resume').onclick = async () => {
316
- const inp = q('#vs2-key'), key = inp.value; inp.value = '';
317
- if (!key) { res('#vs2-resres', 'Enter the operator key.'); return; }
318
- try {
319
- const r = await api('resume', {key});
320
- const s = await window.VK_API('status').catch(() => null);
321
- if (s) data.kernel = Object.assign(data.kernel || {}, s);
322
- draw(); res('#vs2-resres', `<span class="vs2-pill ${r && r.ok ? 'vs2-ok' : 'vs2-bad'}">${r && r.ok ? 'RESUMED' : 'REFUSED'}</span> ${esc((r && r.message) || '')}`);
323
- try { h.refreshTray && h.refreshTray(); } catch (e) {}
324
- } catch (e) { res('#vs2-resres', esc(errMsg(e))); }
325
- };
326
- }
327
- if (cur === 'audit') {
328
- q('#vs2-verify').onclick = async () => {
329
- res('#vs2-audres', T('loading'));
330
- try { const r = await window.VK_API('audit'); const v = (r && r.verify) || {};
331
- res('#vs2-audres', `<span class="vs2-pill ${v.ok ? 'vs2-ok' : 'vs2-bad'}">${v.ok ? 'CHAIN OK' : 'CHAIN BROKEN'}</span> ${esc(v.n || 0)} entries${v.errors && v.errors.length ? ' · ' + esc(v.errors.length) + ' errors' : ''}`);
332
- } catch (e) { res('#vs2-audres', esc(errMsg(e))); }
333
- };
334
- q('#vs2-export').onclick = async () => {
335
- try {
336
- const r = await window.VK_API('audit');
337
- const rows = (r.lines || []).map(l => { try { return JSON.parse(l); } catch (e) { return l; } });
338
- const blob = new Blob([JSON.stringify({exported: new Date().toISOString(), verify: r.verify, entries: rows}, null, 2)], {type: 'application/json'});
339
- const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'vikernel-audit.json';
340
- document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
341
- res('#vs2-audres', `Exported ${rows.length} entries.`);
342
- } catch (e) { res('#vs2-audres', esc(errMsg(e))); }
343
- };
344
- }
345
- if (cur === 'look') {
346
- main.querySelectorAll('[data-theme-set]').forEach(b => b.onclick = () => { LS.set('vk_theme', b.dataset.themeSet); applyTheme(); draw(); });
347
- main.querySelectorAll('[data-accent]').forEach(b => b.onclick = () => { LS.set('vk_accent', b.dataset.accent); document.documentElement.style.setProperty('--indigo', b.dataset.accent); draw(); });
348
- main.querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
349
- LS.set('vk_wall', b.dataset.wall); const d = document.getElementById('desk');
350
- if (d) d.style.background = WALLS[b.dataset.wall] || ''; draw();
351
- });
352
- main.querySelectorAll('[data-lang]').forEach(b => b.onclick = () => { if (b.dataset.lang === LANG) return; LS.set('vk_lang', b.dataset.lang); location.reload(); });
353
- }
354
- if (cur === 'apps' && q('#vs2-remove')) q('#vs2-remove').onclick = async () => {
355
- 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(); }
356
- catch (e) { res('#vs2-appres', esc(errMsg(e))); }
357
- };
358
- }
359
-
360
- go(cur, false);
361
- load();
362
- },
363
- }));
364
- })();
 
 
 
1
+ /* ViKernel Desktop: System Settings app. Non-critical settings change here;
2
+ critical ones are sent to the real ViKernel gate, which denies them. */
3
+ (() => {
4
+ 'use strict';
5
+ const LS = {
6
+ get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : v; } catch (e) { return d; } },
7
+ set(k, v) { try { localStorage.setItem(k, v); } catch (e) {} },
8
+ };
9
+ const LANG = LS.get('vk_lang', 'en') === 'ko' ? 'ko' : 'en';
10
+ const STR = {
11
+ en: {
12
+ title: 'System Settings', about: 'About', model: 'Model & kernel', sec: 'Security & privacy', net: 'Network egress',
13
+ pol: 'Policies', kill: 'Kill switch', audit: 'Audit', look: 'Appearance', apps: 'Apps', upd: 'Updates',
14
+ loading: 'Loading...', err: 'Could not reach the backend. It may be starting; try again.', rate: 'Too many requests. Wait a moment.',
15
+ retry: 'Retry', locked: 'Locked by ViKernel', needKey: 'requires a signed operator key', close: 'Close',
16
+ asking: 'Asking the kernel...', verdict: 'Verdict', rule: 'rule', auditN: 'audit #',
17
+ },
18
+ ko: {
19
+ title: '시스템 설정', about: '정보', model: '모델과 커널', sec: '보안과 개인정보', net: '외부 접속',
20
+ pol: '정책', kill: '킬 스위치', audit: '감사 기록', look: '화면', apps: '앱', upd: '업데이트',
21
+ loading: '불러오는 중...', err: '백엔드에 닿지 못했습니다. 시작 중일 수 있습니다. 다시 시도하십시오.', rate: '요청이 너무 많습니다. 잠시 기다리십시오.',
22
+ retry: '다시 시도', locked: 'ViKernel 이 잠금', needKey: '서명된 운영자 키가 필요합니다', close: '닫기',
23
+ asking: '커널에 묻는 중...', verdict: '판정', rule: '규칙', auditN: '감사 #',
24
+ },
25
+ };
26
+ const T = k => (STR[LANG][k] || STR.en[k] || k);
27
+
28
+ /* Appearance: apply on load */
29
+ const ACCENTS = ['#4338CA', '#0EA5E9', '#10B981', '#F43F5E', '#F59E0B', '#7C3AED'];
30
+ const WALLS = {
31
+ aurora: 'linear-gradient(135deg,#c7d2fe 0%,#e0e7ff 40%,#cffafe 100%)',
32
+ dusk: 'linear-gradient(160deg,#312e81 0%,#7c3aed 55%,#f43f5e 100%)',
33
+ slate: 'linear-gradient(180deg,#0f172a 0%,#334155 100%)',
34
+ };
35
+ function applyTheme() {
36
+ const t = LS.get('vk_theme', 'system');
37
+ const dark = t === 'dark' || (t === 'system' && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
38
+ document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
39
+ }
40
+ function applyLook() {
41
+ applyTheme();
42
+ const a = LS.get('vk_accent', '');
43
+ if (a && ACCENTS.includes(a)) document.documentElement.style.setProperty('--indigo', a);
44
+ const w = LS.get('vk_wall', '');
45
+ const d = document.getElementById('desk');
46
+ if (d && WALLS[w]) d.style.background = WALLS[w];
47
+ }
48
+ applyLook();
49
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', applyLook);
50
+ try { matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); } catch (e) {}
51
+
52
+ const css = `
53
+ .vs2-root{display:flex;height:100%;min-height:0;font-size:13px;color:var(--vs2-fg);background:var(--vs2-bg);
54
+ --vs2-bg:#fff;--vs2-fg:#0f172a;--vs2-mut:#64748b;--vs2-line:#e2e8f0;--vs2-side:#f8fafc;--vs2-card:#fff;--vs2-soft:#f1f5f9}
55
+ [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}
56
+ .vs2-side{width:210px;flex:none;background:var(--vs2-side);border-right:1px solid var(--vs2-line);padding:10px 8px;overflow:auto;outline:none}
57
+ .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}
58
+ .vs2-nav:hover{background:var(--vs2-soft)}.vs2-nav.on{background:var(--indigo);color:#fff}
59
+ .vs2-nav:focus-visible{outline:2px solid var(--indigo);outline-offset:1px}
60
+ .vs2-main{flex:1;min-width:0;overflow:auto;padding:18px 22px}
61
+ .vs2-main h2{margin:0 0 4px;font-size:20px}.vs2-sub{color:var(--vs2-mut);margin:0 0 14px}
62
+ .vs2-card{background:var(--vs2-card);border:1px solid var(--vs2-line);border-radius:12px;padding:12px 14px;margin-bottom:12px}
63
+ .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}
64
+ .vs2-row .vs2-k{flex:1;min-width:0}.vs2-row small{display:block;color:var(--vs2-mut)}
65
+ .vs2-mono{font-family:ui-monospace,Consolas,monospace;font-size:12px;word-break:break-all}
66
+ .vs2-btn{border:1px solid var(--vs2-line);background:var(--vs2-card);color:inherit;border-radius:8px;padding:6px 12px;font:inherit;cursor:pointer}
67
+ .vs2-btn:hover{border-color:var(--indigo)}.vs2-btn.pri{background:var(--indigo);border-color:var(--indigo);color:#fff}
68
+ .vs2-btn.dng{background:#e11d48;border-color:#e11d48;color:#fff}.vs2-btn:disabled{opacity:.5;cursor:default}
69
+ .vs2-sw{position:relative;width:40px;height:22px;flex:none;border-radius:11px;border:0;background:#cbd5e1;cursor:pointer;transition:background .15s}
70
+ .vs2-sw::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:left .15s}
71
+ .vs2-sw[aria-checked=true]{background:var(--indigo)}.vs2-sw[aria-checked=true]::after{left:21px}.vs2-sw:disabled{opacity:.5}
72
+ .vs2-lockrow{cursor:pointer}.vs2-lockrow:hover{background:var(--vs2-soft)}
73
+ .vs2-lk{display:inline-flex;align-items:center;gap:4px;color:#b45309;font-weight:600;font-size:12px}
74
+ .vs2-pill{display:inline-block;padding:2px 10px;border-radius:999px;font-weight:700;font-size:12px}
75
+ .vs2-ok{background:#d1fae5;color:#065f46}.vs2-bad{background:#ffe4e6;color:#9f1239}.vs2-unk{background:var(--vs2-soft);color:var(--vs2-mut)}
76
+ .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}
77
+ .vs2-radio.on{border-color:var(--indigo)}
78
+ .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}
79
+ .vs2-swatch{width:26px;height:26px;border-radius:50%;border:2px solid transparent;cursor:pointer}.vs2-swatch.on{border-color:var(--vs2-fg)}
80
+ .vs2-wall{width:70px;height:44px;border-radius:8px;border:2px solid transparent;cursor:pointer}.vs2-wall.on{border-color:var(--indigo)}
81
+ .vs2-note{color:var(--vs2-mut);font-size:12px;margin-top:6px}.vs2-res{margin-top:8px;font-size:12px}
82
+ .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}
83
+ .vs2-empty{text-align:center;padding:30px 10px;color:var(--vs2-mut)}
84
+ .vs2-modal{position:absolute;inset:0;background:rgba(15,23,42,.45);display:flex;align-items:center;justify-content:center;z-index:5}
85
+ .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}
86
+ .vs2-dlg svg{color:#b45309}
87
+ .vs2-back{display:none}
88
+ @media (max-width:640px){.vs2-root{display:block;overflow:auto}.vs2-side{width:auto;border-right:0}
89
+ .vs2-root.vs2-detail .vs2-side{display:none}.vs2-root:not(.vs2-detail) .vs2-main{display:none}
90
+ .vs2-back{display:inline-block;margin-bottom:10px}.vs2-main{padding:14px 16px}}
91
+ `;
92
+ const LOCK = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>';
93
+ const GEAR = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>';
94
+
95
+ const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
96
+ const api = (p, b) => window.VK_API(p, b === undefined ? undefined : Object.assign({session: window.VK_SID}, b));
97
+ const errMsg = e => (e && e.message === 'rate') ? T('rate') : T('err');
98
+ const kv = k => k ? `${T('verdict')}: <b>${esc(k.verdict)}</b> · ${T('rule')} ${esc(k.rule)}${k.audit_seq != null ? ' · ' + T('auditN') + esc(k.audit_seq) : ''}` : '';
99
+
100
+ window.VK_EXTRA_APPS = window.VK_EXTRA_APPS || [];
101
+ window.VK_EXTRA_APPS.push(h => ({
102
+ id: 'settings', name: () => T('title'), color: '#64748B', icon: GEAR, w: 980, h: 680,
103
+ render(body, win) {
104
+ if (!document.getElementById('vs2-style')) {
105
+ const st = document.createElement('style'); st.id = 'vs2-style'; st.textContent = css; document.head.appendChild(st);
106
+ }
107
+ const SECS = ['about', 'model', 'sec', 'net', 'pol', 'kill', 'audit', 'look', 'apps', 'upd'];
108
+ let cur = LS.get('vk_settings_sec', 'about'); if (!SECS.includes(cur)) cur = 'about';
109
+ let data = null;
110
+ body.innerHTML = '';
111
+ body.style.position = 'relative';
112
+ const root = document.createElement('div'); root.className = 'vs2-root';
113
+ const side = document.createElement('nav'); side.className = 'vs2-side'; side.setAttribute('role', 'tablist'); side.setAttribute('aria-label', T('title'));
114
+ const main = document.createElement('section'); main.className = 'vs2-main'; main.setAttribute('role', 'tabpanel');
115
+ root.append(side, main); body.appendChild(root);
116
+ const toast = m => { try { h.toast(m); } catch (e) {} };
117
+
118
+ SECS.forEach(id => {
119
+ const b = document.createElement('button'); b.className = 'vs2-nav'; b.dataset.id = id; b.setAttribute('role', 'tab');
120
+ b.textContent = T(id); b.onclick = () => go(id, true); side.appendChild(b);
121
+ });
122
+ side.addEventListener('keydown', e => {
123
+ if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return;
124
+ e.preventDefault();
125
+ const i = SECS.indexOf(cur), n = (i + (e.key === 'ArrowDown' ? 1 : SECS.length - 1)) % SECS.length;
126
+ go(SECS[n], false); side.querySelector(`[data-id="${SECS[n]}"]`).focus();
127
+ });
128
+
129
+ function go(id, detail) {
130
+ cur = id; LS.set('vk_settings_sec', id);
131
+ 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; });
132
+ if (detail) root.classList.add('vs2-detail');
133
+ draw();
134
+ }
135
+ async function load() {
136
+ main.innerHTML = `<div class="vs2-empty">${T('loading')}</div>`;
137
+ try { data = await api('settings/get', {}); draw(); }
138
+ catch (e) {
139
+ data = null;
140
+ main.innerHTML = `<div class="vs2-empty">${esc(errMsg(e))}<br><br><button class="vs2-btn pri">${T('retry')}</button></div>`;
141
+ main.querySelector('button').onclick = load;
142
+ }
143
+ }
144
+ function head(title, sub) {
145
+ return `<button class="vs2-btn vs2-back">&larr; ${esc(T('title'))}</button><h2>${esc(title)}</h2>${sub ? `<p class="vs2-sub">${esc(sub)}</p>` : ''}`;
146
+ }
147
+ function lockRow(action, label, sub) {
148
+ return `<div class="vs2-row vs2-lockrow" tabindex="0" role="button" data-lock="${esc(action)}"><div class="vs2-k">${esc(label)}${sub ? `<small>${esc(sub)}</small>` : ''}</div><span class="vs2-lk">${LOCK} Locked</span></div>`;
149
+ }
150
+ function wireLocks() {
151
+ main.querySelectorAll('[data-lock]').forEach(r => {
152
+ r.onclick = () => lockedModal(r.dataset.lock);
153
+ r.onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); lockedModal(r.dataset.lock); } };
154
+ });
155
+ const bk = main.querySelector('.vs2-back'); if (bk) bk.onclick = () => root.classList.remove('vs2-detail');
156
+ }
157
+ function lockedModal(action, pre) {
158
+ const m = document.createElement('div'); m.className = 'vs2-modal';
159
+ m.innerHTML = `<div class="vs2-dlg" role="dialog" aria-modal="true"><div>${LOCK.replace(/14/g, '36')}</div>
160
+ <h3 style="margin:8px 0 4px">${T('locked')}</h3><p style="margin:0 0 10px">${esc(action.replace(/_/g, ' '))}: ${T('needKey')}</p>
161
+ <div class="vs2-res vs2-mono">${pre ? kv(pre.kernel) : T('asking')}</div><br><button class="vs2-btn pri">${T('close')}</button></div>`;
162
+ body.appendChild(m);
163
+ const close = () => m.remove();
164
+ m.querySelector('button').onclick = close; m.onclick = e => { if (e.target === m) close(); };
165
+ m.addEventListener('keydown', e => { if (e.key === 'Escape') close(); });
166
+ m.querySelector('button').focus();
167
+ if (pre) return;
168
+ api('settings/locked', {action}).then(r => {
169
+ m.querySelector('.vs2-res').innerHTML = r && r.kernel ? kv(r.kernel) + (r.kernel.reason ? `<br>${esc(r.kernel.reason)}` : '') : esc(T('err'));
170
+ }).catch(e => { m.querySelector('.vs2-res').textContent = errMsg(e); });
171
+ }
172
+
173
+ const view = {
174
+ about() {
175
+ const k = data.kernel || {};
176
+ const ok = k.verified === true, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
177
+ return head(T('about'), 'ViKernel Desktop, a public web demo.') + `<div class="vs2-card">
178
+ <div class="vs2-row"><div class="vs2-k">Version</div><b>${esc(data.version)}</b></div>
179
+ <div class="vs2-row"><div class="vs2-k">Boot status</div><span class="vs2-pill ${halted ? 'vs2-bad' : ok ? 'vs2-ok' : 'vs2-unk'}">${esc(k.kernel_state || 'unknown')}${ok ? ' · verified' : ''}</span></div>
180
+ <div class="vs2-row"><div class="vs2-k">Audit chain head</div><span class="vs2-mono">${esc(String(k.audit_head || '').slice(0, 16))}${k.audit_head ? '...' : 'none'} (${esc(k.audit_n || 0)} entries)</span></div>
181
+ <div class="vs2-row"><div class="vs2-k">Model</div><span>${esc(data.model || 'unknown')}</span></div>
182
+ <div class="vs2-row"><div class="vs2-k">Gate kernel mode<small>Mode B: CPU reference implementation of the ViKernel gate</small></div><b>Mode B</b></div></div>
183
+ <p class="vs2-note">Mode A (kernel inside the weights) is available on the Glimmer option in Agent Guard.</p>`;
184
+ },
185
+ model() {
186
+ const pref = LS.get('vk_model_pref', 'spark');
187
+ const opt = (id, t, s, active) => `<button class="vs2-radio ${pref === id ? 'on' : ''}" data-pref="${id}" role="radio" aria-checked="${pref === id}"><b>${esc(t)}</b>${active ? ' <span class="vs2-pill vs2-ok">active</span>' : ''}<br><small class="vs2-note">${esc(s)}</small></button>`;
188
+ return head(T('model'), 'Which model drives the agent, and where the kernel runs.') +
189
+ opt('spark', 'Meta Muse Spark 1.3 (API) + ViKernel gate', 'Every tool call is decided by the ViKernel gate (Mode B).', true) +
190
+ opt('glimmer_b', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode B', 'Local open weights, gate on the CPU reference.') +
191
+ opt('glimmer_a', 'Meta Muse Glimmer 30B + ViKernel in-weights, Mode A', 'The kernel runs inside the model weights.') +
192
+ (pref !== 'spark' ? `<div class="vs2-card">Open Agent Guard to use it. <button class="vs2-btn pri" data-open="guard">Open Agent Guard</button></div>` : '');
193
+ },
194
+ sec() {
195
+ const a = data.app;
196
+ if (!a) return head(T('sec'), 'Per-app permissions.') + `<div class="vs2-card vs2-empty">No app is installed in this session.<br><br><button class="vs2-btn pri" data-open="appstore">Open the App Store</button></div>`;
197
+ const rows = a.declared.map(t => { const on = a.current.includes(t);
198
+ return `<div class="vs2-row"><div class="vs2-k"><span class="vs2-mono">${esc(t)}</span><small>${on ? 'Allowed' : 'Revoked: the kernel denies it (rule manifest)'}</small></div><button class="vs2-sw" role="switch" aria-label="${esc(t)}" aria-checked="${on}" data-tool="${esc(t)}"></button></div>`; }).join('');
199
+ return head(T('sec'), 'Like a phone OS: turn off any permission the app declared.') + `<div class="vs2-card"><b>${esc(a.name)}</b> <small class="vs2-note">${esc(a.id)}</small>
200
+ ${rows || '<div class="vs2-empty">This app declared no tools.</div>'}
201
+ ${a.paths.length ? `<div class="vs2-note">File paths: <span class="vs2-mono">${a.paths.map(esc).join(', ')}</span></div>` : ''}
202
+ <div class="vs2-res" id="vs2-permres"></div></div>
203
+ <p class="vs2-note">Try it: Ask the agent in Agent Guard to use this tool; it will be denied (rule manifest).</p>`;
204
+ },
205
+ net() {
206
+ const l = data.settings.egress_allow;
207
+ return head(T('net'), 'The agent may only reach these domains. Everything else is denied (rule egress).') + `<div class="vs2-card">
208
+ ${l.length ? l.map(d => `<div class="vs2-row"><div class="vs2-k vs2-mono">${esc(d)}</div><button class="vs2-btn" data-rm="${esc(d)}">Remove</button></div>`).join('') : '<div class="vs2-empty">The allowlist is empty: all egress is denied.</div>'}
209
+ <div class="vs2-row"><input class="vs2-in" id="vs2-dom" placeholder="docs.python.org" aria-label="Domain" style="flex:1"><button class="vs2-btn pri" id="vs2-add">Add</button></div>
210
+ <div class="vs2-res" id="vs2-netres"></div></div>
211
+ <div class="vs2-card">${lockRow('allow_all_egress', 'Allow all egress', 'Wildcards, IP addresses, localhost and metadata hosts count as this.')}</div>`;
212
+ },
213
+ pol() {
214
+ const p = data.settings.policy;
215
+ const tog = (k, l) => `<div class="vs2-row"><div class="vs2-k">${esc(l)}</div><button class="vs2-sw" role="switch" aria-label="${esc(l)}" aria-checked="${!!p[k]}" data-pol="${k}"></button></div>`;
216
+ return head(T('pol'), 'The rules the kernel enforces on every action.') +
217
+ `<div class="vs2-card"><table class="vs2-table"><tr><th>Rule</th><th>Name</th><th>What it does</th></tr>${data.policies.map(r => `<tr><td><b>${esc(r.id)}</b></td><td>${esc(r.name)}</td><td>${esc(r.desc)}</td></tr>`).join('')}</table></div>
218
+ <div class="vs2-card"><b>Display and agent (not critical)</b>
219
+ ${tog('verbose_audit_view', 'Verbose audit view')}${tog('show_kernel_ms', 'Show kernel time (ms)')}
220
+ <div class="vs2-row"><div class="vs2-k">Agent step limit<small>1 to 12</small></div><input class="vs2-in" type="number" min="1" max="12" id="vs2-steps" value="${esc(p.agent_step_limit)}" style="width:70px"></div>
221
+ <div class="vs2-res" id="vs2-polres"></div></div>
222
+ <div class="vs2-card"><b>Critical rules</b>${data.policies.filter(r => r.critical).map(r => lockRow('lower_policy', 'Turn off rule ' + r.id + ' (' + r.name + ')')).join('')}</div>`;
223
+ },
224
+ kill() {
225
+ const k = data.kernel || {}, halted = String(k.kernel_state || '').toUpperCase().includes('HALT');
226
+ return head(T('kill'), 'The kill switch sits above every app. When halted, every action is denied.') + `<div class="vs2-card">
227
+ <div class="vs2-row"><div class="vs2-k">Kernel state</div><span class="vs2-pill ${halted ? 'vs2-bad' : 'vs2-ok'}" id="vs2-kstate">${esc(k.kernel_state || 'unknown')}</span></div>
228
+ <div class="vs2-row"><div class="vs2-k">Test the kill switch<small>Halts the kernel for this demo. Resume needs the operator key.</small></div><button class="vs2-btn dng" id="vs2-kill">Test kill switch</button></div>
229
+ <div class="vs2-res" id="vs2-killres"></div></div>
230
+ <div class="vs2-card"><b>Operator key</b><p class="vs2-note">The demo key is published in the README for testing. A real deployment keeps it offline.</p>
231
+ <div class="vs2-row"><input class="vs2-in" type="password" id="vs2-key" autocomplete="off" placeholder="operator key" aria-label="Operator key" style="flex:1"><button class="vs2-btn pri" id="vs2-resume">Resume</button></div>
232
+ <div class="vs2-res" id="vs2-resres"></div></div>
233
+ <div class="vs2-card">${lockRow('disable_kill_switch', 'Disable kill switch')}</div>`;
234
+ },
235
+ audit() {
236
+ return head(T('audit'), 'Every kernel decision is sealed in a hash chain.') + `<div class="vs2-card">
237
+ <div class="vs2-row"><div class="vs2-k">Retention</div><span>kept for the session; exported on demand</span></div>
238
+ <div class="vs2-row"><div class="vs2-k">Verify the chain</div><button class="vs2-btn" id="vs2-verify">Verify</button></div>
239
+ <div class="vs2-row"><div class="vs2-k">Export</div><button class="vs2-btn" id="vs2-export">Export JSON</button></div>
240
+ <div class="vs2-res" id="vs2-audres"></div></div>
241
+ <div class="vs2-card">${lockRow('disable_audit', 'Turn off audit')}</div>`;
242
+ },
243
+ look() {
244
+ const t = LS.get('vk_theme', 'system'), a = LS.get('vk_accent', ACCENTS[0]), w = LS.get('vk_wall', '');
245
+ return head(T('look'), 'Saved in this browser only.') + `<div class="vs2-card">
246
+ <div class="vs2-row"><div class="vs2-k">Theme</div>${['light', 'dark', 'system'].map(x => `<button class="vs2-btn ${t === x ? 'pri' : ''}" data-theme-set="${x}">${x[0].toUpperCase() + x.slice(1)}</button>`).join('')}</div>
247
+ <div class="vs2-row"><div class="vs2-k">Accent color</div>${ACCENTS.map(c => `<button class="vs2-swatch ${a === c ? 'on' : ''}" style="background:${c}" data-accent="${c}" aria-label="Accent ${c}"></button>`).join('')}</div>
248
+ <div class="vs2-row"><div class="vs2-k">Wallpaper</div><button class="vs2-wall ${!w ? 'on' : ''}" style="background:var(--vs2-soft)" data-wall="" aria-label="Default"></button>${Object.keys(WALLS).map(k => `<button class="vs2-wall ${w === k ? 'on' : ''}" style="background:${WALLS[k]}" data-wall="${k}" aria-label="${k}"></button>`).join('')}</div>
249
+ <div class="vs2-row"><div class="vs2-k">Display scale</div>${window.VK_DISPLAY ? window.VK_DISPLAY.modes.map(m => `<button class="vs2-btn ${window.VK_DISPLAY.get() === m ? 'pri' : ''}" data-scale="${m}">${m === 'auto' ? 'Auto' : Math.round(parseFloat(m) * 100) + '%'}</button>`).join('') : ''}</div>
250
+ <div class="vs2-row"><div class="vs2-k">Language / 언어</div><button class="vs2-btn ${LANG === 'en' ? 'pri' : ''}" data-lang="en">EN</button><button class="vs2-btn ${LANG === 'ko' ? 'pri' : ''}" data-lang="ko">KO</button></div></div>`;
251
+ },
252
+ apps() {
253
+ const a = data.app;
254
+ return head(T('apps'), 'Agent apps installed in this session.') + `<div class="vs2-card">${a ? `<div class="vs2-row"><div class="vs2-k"><b>${esc(a.name)}</b><small>${esc(a.id)} · ${a.current.length}/${a.declared.length} tools allowed</small></div><button class="vs2-btn dng" id="vs2-remove">Remove</button></div>` : '<div class="vs2-empty">No app installed.</div>'}
255
+ <div class="vs2-res" id="vs2-appres"></div></div><button class="vs2-btn pri" data-open="appstore">Open the App Store</button>`;
256
+ },
257
+ upd() {
258
+ const log = [['v0.4', 'App Store, Browser, Settings, Terminal, Minesweeper, product pass'], ['v0.3', 'App Store preview, VDR packages'],
259
+ ['v0.2', 'Live Meta Muse agent with Agent Guard'], ['v0.1', 'Desktop shell']];
260
+ return head(T('upd'), 'You are on ' + (data.version || 'v0.4') + '.') + `<div class="vs2-card">${log.map(([v, s]) => `<div class="vs2-row"><div class="vs2-k"><b>${v}</b><small>${esc(s)}</small></div></div>`).join('')}</div>`;
261
+ },
262
+ };
263
+
264
+ function draw() {
265
+ if (!data) return;
266
+ main.innerHTML = view[cur]();
267
+ main.scrollTop = 0;
268
+ wireLocks();
269
+ main.querySelectorAll('[data-open]').forEach(b => b.onclick = () => { try { h.openApp(b.dataset.open); } catch (e) {} });
270
+ const q = s => main.querySelector(s);
271
+ const res = (id, html) => { const e = q(id); if (e) e.innerHTML = html; };
272
+ if (cur === 'model') main.querySelectorAll('[data-pref]').forEach(b => b.onclick = () => { LS.set('vk_model_pref', b.dataset.pref); draw(); });
273
+ if (cur === 'sec') main.querySelectorAll('[data-tool]').forEach(b => b.onclick = async () => {
274
+ const on = b.getAttribute('aria-checked') === 'true'; b.disabled = true;
275
+ try {
276
+ const r = await api(on ? 'settings/revoke' : 'settings/grant', {tool: b.dataset.tool});
277
+ if (r && r.app) { data.app = r.app; draw(); res('#vs2-permres', `${on ? 'Revoked' : 'Granted'}: <span class="vs2-mono">${esc(b.dataset.tool)}</span>`); }
278
+ else res('#vs2-permres', esc((r && r.error) || T('err')));
279
+ try { h.refreshTray && h.refreshTray(); } catch (e) {}
280
+ } catch (e) { b.disabled = false; res('#vs2-permres', esc(errMsg(e))); }
281
+ });
282
+ if (cur === 'net') {
283
+ const edit = async body => {
284
+ try {
285
+ const r = await api('settings/egress', body);
286
+ if (r && r.locked) { lockedModal(r.action, r); return; }
287
+ if (r && r.egress_allow) { data.settings.egress_allow = r.egress_allow; draw(); }
288
+ else res('#vs2-netres', esc((r && r.error) || T('err')));
289
+ } catch (e) { res('#vs2-netres', esc(e.message.startsWith('http 400') ? 'Invalid domain. Use a name like docs.python.org.' : errMsg(e))); }
290
+ };
291
+ q('#vs2-add').onclick = () => { const v = q('#vs2-dom').value.trim(); if (v) edit({add: v}); };
292
+ q('#vs2-dom').onkeydown = e => { if (e.key === 'Enter') q('#vs2-add').click(); };
293
+ main.querySelectorAll('[data-rm]').forEach(b => b.onclick = () => edit({remove: b.dataset.rm}));
294
+ }
295
+ if (cur === 'pol') {
296
+ const set = async (key, value) => {
297
+ try {
298
+ const r = await api('settings/policy', {key, value});
299
+ if (r && r.locked) { lockedModal(r.action, r); return; }
300
+ if (r && r.policy) { data.settings.policy = r.policy; draw(); res('#vs2-polres', 'Saved.'); }
301
+ } catch (e) { res('#vs2-polres', esc(e.message.startsWith('http 400') ? 'Value out of range.' : errMsg(e))); }
302
+ };
303
+ main.querySelectorAll('[data-pol]').forEach(b => b.onclick = () => set(b.dataset.pol, b.getAttribute('aria-checked') !== 'true'));
304
+ q('#vs2-steps').onchange = e => set('agent_step_limit', parseInt(e.target.value, 10));
305
+ }
306
+ if (cur === 'kill') {
307
+ q('#vs2-kill').onclick = async () => {
308
+ try {
309
+ const r = await api('kill', {});
310
+ const s = await window.VK_API('status').catch(() => null);
311
+ if (s) data.kernel = Object.assign(data.kernel || {}, s);
312
+ draw(); res('#vs2-killres', `<span class="vs2-pill vs2-bad">HALTED</span> ${esc((r && r.message) || '')}`);
313
+ try { h.refreshTray && h.refreshTray(); } catch (e) {}
314
+ } catch (e) { res('#vs2-killres', esc(errMsg(e))); }
315
+ };
316
+ q('#vs2-resume').onclick = async () => {
317
+ const inp = q('#vs2-key'), key = inp.value; inp.value = '';
318
+ if (!key) { res('#vs2-resres', 'Enter the operator key.'); return; }
319
+ try {
320
+ const r = await api('resume', {key});
321
+ const s = await window.VK_API('status').catch(() => null);
322
+ if (s) data.kernel = Object.assign(data.kernel || {}, s);
323
+ draw(); res('#vs2-resres', `<span class="vs2-pill ${r && r.ok ? 'vs2-ok' : 'vs2-bad'}">${r && r.ok ? 'RESUMED' : 'REFUSED'}</span> ${esc((r && r.message) || '')}`);
324
+ try { h.refreshTray && h.refreshTray(); } catch (e) {}
325
+ } catch (e) { res('#vs2-resres', esc(errMsg(e))); }
326
+ };
327
+ }
328
+ if (cur === 'audit') {
329
+ q('#vs2-verify').onclick = async () => {
330
+ res('#vs2-audres', T('loading'));
331
+ try { const r = await window.VK_API('audit'); const v = (r && r.verify) || {};
332
+ res('#vs2-audres', `<span class="vs2-pill ${v.ok ? 'vs2-ok' : 'vs2-bad'}">${v.ok ? 'CHAIN OK' : 'CHAIN BROKEN'}</span> ${esc(v.n || 0)} entries${v.errors && v.errors.length ? ' · ' + esc(v.errors.length) + ' errors' : ''}`);
333
+ } catch (e) { res('#vs2-audres', esc(errMsg(e))); }
334
+ };
335
+ q('#vs2-export').onclick = async () => {
336
+ try {
337
+ const r = await window.VK_API('audit');
338
+ const rows = (r.lines || []).map(l => { try { return JSON.parse(l); } catch (e) { return l; } });
339
+ const blob = new Blob([JSON.stringify({exported: new Date().toISOString(), verify: r.verify, entries: rows}, null, 2)], {type: 'application/json'});
340
+ const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'vikernel-audit.json';
341
+ document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
342
+ res('#vs2-audres', `Exported ${rows.length} entries.`);
343
+ } catch (e) { res('#vs2-audres', esc(errMsg(e))); }
344
+ };
345
+ }
346
+ if (cur === 'look') {
347
+ main.querySelectorAll('[data-theme-set]').forEach(b => b.onclick = () => { LS.set('vk_theme', b.dataset.themeSet); applyTheme(); draw(); });
348
+ main.querySelectorAll('[data-accent]').forEach(b => b.onclick = () => { LS.set('vk_accent', b.dataset.accent); document.documentElement.style.setProperty('--indigo', b.dataset.accent); draw(); });
349
+ main.querySelectorAll('[data-scale]').forEach(b => b.onclick = () => { window.VK_DISPLAY.set(b.dataset.scale); location.reload(); });
350
+ main.querySelectorAll('[data-wall]').forEach(b => b.onclick = () => {
351
+ LS.set('vk_wall', b.dataset.wall); const d = document.getElementById('desk');
352
+ if (d) d.style.background = WALLS[b.dataset.wall] || ''; draw();
353
+ });
354
+ main.querySelectorAll('[data-lang]').forEach(b => b.onclick = () => { if (b.dataset.lang === LANG) return; LS.set('vk_lang', b.dataset.lang); location.reload(); });
355
+ }
356
+ if (cur === 'apps' && q('#vs2-remove')) q('#vs2-remove').onclick = async () => {
357
+ 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(); }
358
+ catch (e) { res('#vs2-appres', esc(errMsg(e))); }
359
+ };
360
+ }
361
+
362
+ go(cur, false);
363
+ load();
364
+ },
365
+ }));
366
+ })();