feat: implement Windows service for parental control application

This commit is contained in:
2026-08-02 16:24:53 +07:00
parent 32d2553380
commit 229a02b543
10 changed files with 1111 additions and 272 deletions
+27 -10
View File
@@ -5,6 +5,11 @@ async function api(path, body) {
? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }
: {};
const res = await fetch(path, init);
// сессия истекла — иначе открытая вкладка молча перестала бы обновляться
if (res.status === 401) {
location.href = '/login';
throw new Error('нужен вход');
}
if (!res.ok) throw new Error(`${path}: ${res.status}`);
return res.json();
}
@@ -89,25 +94,32 @@ 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;
if (i.phase === 'checking') return 'проверка…';
if (i.phase === 'installing') return `устанавливается ${i.latest}, панель перезапустится`;
if (i.error) return i.error;
if (i.available) return `доступна версия ${i.latest}`;
return 'установлена последняя версия';
});
return { busy, status };
return {
status,
checking: computed(() => props.info.phase === 'checking'),
bad: computed(() => Boolean(props.info.error)),
};
},
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>
<span :class="bad ? 'warn' : 'muted'">{{ status }}</span>
</div>
<button :disabled="busy" @click="$emit('check')">Проверить обновления</button>
<button :disabled="checking" @click="$emit('check')">Проверить обновления</button>
</div>
<div v-if="info.installer" class="muted" style="margin-top:8px">
Установщик, {{ info.installer.time }}:
<span :class="{ warn: !info.installer.ok }">{{ info.installer.message }}</span>
</div>
</section>`,
};
@@ -243,7 +255,9 @@ const App = {
const { info: update, load: loadUpdate, check: checkUpdate } = useUpdate();
onMounted(loadRules);
usePolling(() => { loadProcesses(); loadLog(); loadUpdate(); }, 3000);
usePolling(() => { loadProcesses(); loadLog(); }, 3000);
// версия меняется раз в сутки в лучшем случае — частить незачем
usePolling(loadUpdate, 30000);
// в whitelist «заблокирован» означает отсутствие в allowed — действия зеркальные
const toggle = async (g) => {
@@ -265,7 +279,10 @@ const App = {
return { rules, groups, lines, update, whitelist, save, remove, toggle, kill, scheduleRandom, checkUpdate };
},
template: `
<h1>Родительский контроль</h1>
<div class="row" style="justify-content:space-between">
<h1>Родительский контроль</h1>
<form method="post" action="/logout"><button>Выйти</button></form>
</div>
<update-bar :info="update" @check="checkUpdate" />
<rules-panel :rules="rules" @update="save" @remove="remove" />
<random-kill @schedule="scheduleRandom" />
+1
View File
@@ -17,6 +17,7 @@
button { background:#2a2f3a; color:#e6e8eb; border:1px solid #3a4150;
border-radius:6px; padding:4px 10px; cursor:pointer; font-size:13px; }
button:hover { background:#343a47; }
button:disabled { opacity:.5; cursor:default; background:#2a2f3a; }
button.danger { border-color:#6b2b2b; color:#ff9b9b; }
button.danger:hover { background:#3a1f1f; }
input, select { background:#14161a; color:#e6e8eb; border:1px solid #3a4150;
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Вход — Родительский контроль</title>
<style>
:root { color-scheme: dark; }
* { box-sizing: border-box; }
body { margin:0; min-height:100vh; display:grid; place-items:center;
background:#14161a; color:#e6e8eb;
font:14px/1.5 system-ui, "Segoe UI", sans-serif; }
form { background:#1c1f26; border:1px solid #2a2f3a; border-radius:10px;
padding:22px; width:min(320px, 90vw); }
h1 { font-size:16px; margin:0 0 14px; }
label { display:block; color:#9aa4b2; font-size:13px; margin-bottom:6px; }
input { width:100%; background:#14161a; color:#e6e8eb; border:1px solid #3a4150;
border-radius:6px; padding:8px; font-size:14px; }
button { width:100%; margin-top:14px; background:#2a2f3a; color:#e6e8eb;
border:1px solid #3a4150; border-radius:6px; padding:8px;
cursor:pointer; font-size:14px; }
button:hover { background:#343a47; }
p.msg { margin:12px 0 0; color:#e0b872; font-size:13px; }
</style>
</head>
<body>
<form method="post" action="/login">
<h1>Родительский контроль</h1>
<label for="password">Пароль</label>
<input id="password" name="password" type="password" autofocus required>
<button type="submit">Войти</button>
<p class="msg"><!--MESSAGE--></p>
</form>
</body>
</html>