const { createApp, ref, computed, onMounted, onUnmounted } = Vue; async function api(path, body) { const init = body ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) } : {}; const res = await fetch(path, init); if (!res.ok) throw new Error(`${path}: ${res.status}`); return res.json(); } /* ── composables ───────────────────────────────────────────── */ function useRules() { const rules = ref({ mode: 'blacklist', blocked: [], allowed: [], enforce: true }); const load = async () => { rules.value = await api('/rules'); }; const block = (name) => api('/rules/block', { name }).then(load); const allow = (name) => api('/rules/allow', { name }).then(load); const remove = (name) => api('/rules/remove', { name }).then(load); // patch мержим в текущее состояние: сервер принимает объект правил целиком const save = (patch) => api('/rules', { ...rules.value, ...patch }).then(load); return { rules, load, block, allow, remove, save }; } function useProcesses() { const raw = ref([]); const load = async () => { raw.value = await api('/processes'); }; const kill = (name) => api('/kill', { name }).then(load); // одноимённые процессы схлопываем — иначе двадцать строк chrome.exe const groups = computed(() => { const map = new Map(); for (const p of raw.value) { const key = p.name.toLowerCase(); if (!map.has(key)) map.set(key, { key, name: p.name, count: 0, mem: 0, blocked: p.blocked }); const g = map.get(key); g.count += 1; g.mem += p.memory_mb; } return [...map.values()].sort((a, b) => b.mem - a.mem); }); return { groups, load, kill }; } function useLog() { const lines = ref([]); const load = async () => { lines.value = await api('/log'); }; return { lines, load }; } function useUpdate() { const info = ref({ current: '', latest: null, available: false, phase: 'idle', checked_at: null, error: null }); const load = async () => { info.value = await api('/update'); }; // после ручной проверки состояние меняется не мгновенно — перечитываем чуть позже const check = () => api('/update/check', {}).then(() => setTimeout(load, 1500)); return { info, load, check }; } function usePolling(fn, ms) { let id = null; onMounted(() => { fn(); id = setInterval(fn, ms); }); onUnmounted(() => clearInterval(id)); } /* ── компоненты ────────────────────────────────────────────── */ const NameChips = { props: { title: { type: String, required: true }, names: { type: Array, default: () => [] }, }, emits: ['remove'], setup(props) { return { sorted: computed(() => [...props.names].sort()) }; }, template: `
{{ title }}
пусто {{ name }}
`, }; const UpdateBar = { props: { info: { type: Object, required: true } }, emits: ['check'], setup(props) { const busy = computed(() => ['checking', 'downloading', 'installing'].includes(props.info.phase)); const status = computed(() => { const i = props.info; const phases = { checking: 'проверка…', downloading: 'загрузка…', installing: 'установка…' }; if (phases[i.phase]) return phases[i.phase]; if (i.error) return 'не удалось проверить обновления'; if (i.available && i.latest && i.latest !== i.current) return 'доступна версия ' + i.latest; return 'установлена последняя версия'; }); return { busy, status }; }, template: `

Версия {{ info.current || '—' }}

{{ status }}
`, }; const RulesPanel = { components: { NameChips }, props: { rules: { type: Object, required: true } }, emits: ['update', 'remove'], template: `

Правила

Белый список пуст — сервер намеренно ничего не завершает, иначе снесло бы систему.

`, }; const RandomKill = { emits: ['schedule'], setup(_, { emit }) { const name = ref(''); const min = ref(5); const max = ref(30); const submit = () => { const value = name.value.trim(); if (!value) return; emit('schedule', { name: value, min_secs: Math.max(0, min.value) * 60, max_secs: Math.max(1, max.value) * 60, }); name.value = ''; }; return { name, min, max, submit }; }, template: `

Случайное завершение

мин
`, }; const ProcessTable = { props: { groups: { type: Array, default: () => [] }, whitelist: { type: Boolean, default: false }, }, emits: ['toggle', 'kill'], setup(props) { const filter = ref(''); const visible = computed(() => { const q = filter.value.trim().toLowerCase(); return q ? props.groups.filter((g) => g.key.includes(q)) : props.groups; }); return { filter, visible }; }, template: `

Процессы

{{ visible.length }} из {{ groups.length }}
ИмяЭкз.Память
{{ g.name }} {{ g.count }} {{ g.mem }} МБ
`, }; const EventLog = { props: { lines: { type: Array, default: () => [] } }, setup(props) { return { recent: computed(() => props.lines.slice(-40).reverse().join('\n')) }; }, template: `

Журнал

{{ recent || 'пусто' }}
`, }; /* ── корневой компонент ────────────────────────────────────── */ const App = { components: { UpdateBar, RulesPanel, RandomKill, ProcessTable, EventLog }, setup() { const { rules, load: loadRules, block, allow, remove, save } = useRules(); const { groups, load: loadProcesses, kill } = useProcesses(); const { lines, load: loadLog } = useLog(); const { info: update, load: loadUpdate, check: checkUpdate } = useUpdate(); onMounted(loadRules); usePolling(() => { loadProcesses(); loadLog(); loadUpdate(); }, 3000); // в whitelist «заблокирован» означает отсутствие в allowed — действия зеркальные const toggle = async (g) => { if (rules.value.mode === 'whitelist') { await (g.blocked ? allow(g.name) : remove(g.name)); } else { await (g.blocked ? remove(g.name) : block(g.name)); } loadProcesses(); }; const scheduleRandom = async (payload) => { await api('/kill/random', payload); loadLog(); }; const whitelist = computed(() => rules.value.mode === 'whitelist'); return { rules, groups, lines, update, whitelist, save, remove, toggle, kill, scheduleRandom, checkUpdate }; }, template: `

Родительский контроль

`, }; createApp(App).mount('#app');