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: `
`,
};
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');