Files
syshelper/static/app.js
T
2026-08-02 13:47:00 +07:00

276 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: `
<div class="chips">
<div class="muted">{{ title }}</div>
<span v-if="!sorted.length" class="muted">пусто</span>
<span v-for="name in sorted" :key="name" class="chip">
{{ name }}
<button title="убрать из списка" @click="$emit('remove', name)">×</button>
</span>
</div>`,
};
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: `
<section class="card">
<div class="row" style="justify-content:space-between">
<div>
<h2 style="margin:0">Версия {{ info.current || '—' }}</h2>
<span :class="info.error ? 'warn' : 'muted'">{{ status }}</span>
</div>
<button :disabled="busy" @click="$emit('check')">Проверить обновления</button>
</div>
</section>`,
};
const RulesPanel = {
components: { NameChips },
props: { rules: { type: Object, required: true } },
emits: ['update', 'remove'],
template: `
<section class="card">
<h2>Правила</h2>
<div class="row">
<label>Режим:
<select :value="rules.mode" @change="$emit('update', { mode: $event.target.value })">
<option value="blacklist">чёрный список</option>
<option value="whitelist">белый список</option>
</select>
</label>
<label>
<input type="checkbox" :checked="rules.enforce"
@change="$emit('update', { enforce: $event.target.checked })">
применять правила
</label>
</div>
<p v-if="rules.mode === 'whitelist' && !rules.allowed.length" class="warn">
Белый список пуст — сервер намеренно ничего не завершает, иначе снесло бы систему.
</p>
<name-chips title="Заблокированы" :names="rules.blocked" @remove="$emit('remove', $event)" />
<name-chips title="Разрешены" :names="rules.allowed" @remove="$emit('remove', $event)" />
</section>`,
};
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: `
<section class="card">
<h2>Случайное завершение</h2>
<div class="row">
<input v-model="name" placeholder="game.exe" style="flex:1;min-width:140px"
@keyup.enter="submit">
<input v-model.number="min" type="number" min="0" style="width:70px"> —
<input v-model.number="max" type="number" min="1" style="width:70px">
<span class="muted">мин</span>
<button @click="submit">Запланировать</button>
</div>
</section>`,
};
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: `
<section class="card">
<h2>Процессы</h2>
<div class="row" style="margin-bottom:10px">
<input v-model="filter" placeholder="фильтр по имени" style="flex:1">
<span class="muted">{{ visible.length }} из {{ groups.length }}</span>
</div>
<div class="scroll">
<table>
<thead>
<tr><th>Имя</th><th>Экз.</th><th>Память</th><th></th></tr>
</thead>
<tbody>
<tr v-for="g in visible" :key="g.key" :class="{ blocked: g.blocked }">
<td>{{ g.name }}</td>
<td>{{ g.count }}</td>
<td>{{ g.mem }} МБ</td>
<td class="right">
<button @click="$emit('toggle', g)">
{{ g.blocked ? (whitelist ? 'Внести в белый список' : 'Разрешить') : 'Блокировать' }}
</button>
<button class="danger" style="margin-left:6px" @click="$emit('kill', g.name)">
Убить
</button>
</td>
</tr>
</tbody>
</table>
</div>
</section>`,
};
const EventLog = {
props: { lines: { type: Array, default: () => [] } },
setup(props) {
return { recent: computed(() => props.lines.slice(-40).reverse().join('\n')) };
},
template: `
<section class="card">
<h2>Журнал</h2>
<pre>{{ recent || 'пусто' }}</pre>
</section>`,
};
/* ── корневой компонент ────────────────────────────────────── */
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: `
<h1>Родительский контроль</h1>
<update-bar :info="update" @check="checkUpdate" />
<rules-panel :rules="rules" @update="save" @remove="remove" />
<random-kill @schedule="scheduleRandom" />
<process-table :groups="groups" :whitelist="whitelist" @toggle="toggle" @kill="kill" />
<event-log :lines="lines" />`,
};
createApp(App).mount('#app');