2 Commits
Author SHA1 Message Date
maeneko 86dabfe3a0 Release 0.5.2
release / release (push) Canceled after 0s
ci / check (push) Canceled after 2m33s
2026-10-30 00:16:11 +03:00
maeneko 6fa54041f6 Prometheus install: wait until the panel sees it before stopping the poll
The status handler probed Prometheus before reading the job, so a poll
could get exit 0 with ready=false; the page then stopped polling and the
Monitor tab waited for App's 60 s check.
2026-10-30 00:16:11 +03:00
10 changed files with 29 additions and 16 deletions
+1 -1
View File
@@ -11,4 +11,4 @@
BRAND=Forgetting BRAND=Forgetting
CHANNEL=Beta CHANNEL=Beta
VERSION=0.5.1 VERSION=0.5.2
+1 -1
View File
@@ -377,7 +377,7 @@ TOKEN=$(curl -s -X POST http://HOST:PORT/login \
| Метод | Ответ | | Метод | Ответ |
|---|---| |---|---|
| `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.5.1" }` — имя и версия панели для вордмарка; за авторизацией, чтобы сканер не опознал панель | | `GET /ui/brand` | `{ "brand": "Forgetting", "channel": "Beta", "version": "0.5.2" }` — имя и версия панели для вордмарка; за авторизацией, чтобы сканер не опознал панель |
| `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` | | `GET /health` | `{ "status", "server", "ip", "gen", "awg": { "status", "peers", "module", "tools" } }`. `200`, если интерфейс поднят, иначе `503` и `status: "degraded"` |
| `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` | | `GET /awg/status` | `{ "up": true, "peers": 3, "publicKey": "…" }` |
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "awg-agent", "name": "awg-agent",
"version": "0.5.1", "version": "0.5.2",
"private": true, "private": true,
"description": "Агент ноды: держит исходящее WSS-соединение к core и исполняет запросы на локальном awg-ctrl", "description": "Агент ноды: держит исходящее WSS-соединение к core и исполняет запросы на локальном awg-ctrl",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "awg-control", "name": "awg-control",
"version": "0.5.1", "version": "0.5.2",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
+2 -2
View File
@@ -15,7 +15,7 @@ const now = () => Math.floor(Date.now() / 1000);
const rnd = (n: number) => Math.floor(Math.random() * n); const rnd = (n: number) => Math.floor(Math.random() * n);
const b64 = (n: number) => crypto.randomBytes(n).toString("base64"); const b64 = (n: number) => crypto.randomBytes(n).toString("base64");
const GEN = "3.1"; const GEN = "3.1";
const MOCK_VERSION = "0.5.1"; const MOCK_VERSION = "0.5.2";
// ── пользователи (vpn://) ── // ── пользователи (vpn://) ──
interface U { name: string; ip: string; pub_key: string; vpn_key: string; key_gen: string; online: boolean; lastHandshake: number; rx: number; tx: number } interface U { name: string; ip: string; pub_key: string; vpn_key: string; key_gen: string; online: boolean; lastHandshake: number; rx: number; tx: number }
@@ -40,7 +40,7 @@ const mnodes: MNode[] = [
{ id: 1, name: "Frankfurt", ip: "198.51.100.4", gen: "2", online: true, pending: false, version: "0.5.0" }, { id: 1, name: "Frankfurt", ip: "198.51.100.4", gen: "2", online: true, pending: false, version: "0.5.0" },
{ id: 2, name: "Tokyo", ip: "", gen: "2", online: false, pending: true, version: null }, { id: 2, name: "Tokyo", ip: "", gen: "2", online: false, pending: true, version: null },
{ id: 3, name: "Amsterdam", ip: "192.0.2.10", gen: "3.1", online: false, pending: false, version: "0.4.5" }, // старше 0.5.0: хаб не подключает { id: 3, name: "Amsterdam", ip: "192.0.2.10", gen: "3.1", online: false, pending: false, version: "0.4.5" }, // старше 0.5.0: хаб не подключает
{ id: 4, name: "Helsinki", ip: "192.0.2.44", gen: "3.1", online: true, pending: false, version: "0.5.1" }, // новее панели — жёлтый чип { id: 4, name: "Helsinki", ip: "192.0.2.44", gen: "3.1", online: true, pending: false, version: "0.5.3" }, // новее панели — жёлтый чип
]; ];
let nodeId = 3; let nodeId = 3;
const joinStr = (id: number) => "awgjoin://" + Buffer.from(JSON.stringify({ v: 1, h: "localhost", p: 8443, pin: crypto.randomBytes(32).toString("base64url"), n: id, s: crypto.randomBytes(16).toString("base64url") })).toString("base64url"); const joinStr = (id: number) => "awgjoin://" + Buffer.from(JSON.stringify({ v: 1, h: "localhost", p: 8443, pin: crypto.randomBytes(32).toString("base64url"), n: id, s: crypto.randomBytes(16).toString("base64url") })).toString("base64url");
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "awg-ui", "name": "awg-ui",
"version": "0.5.1", "version": "0.5.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+14 -6
View File
@@ -20,6 +20,7 @@ export interface MonitorStatus {
// Прогресс операции из строк «N/M шаг», которые печатает сам установщик (cli/src/monitor.ts). Шаг N считаем // Прогресс операции из строк «N/M шаг», которые печатает сам установщик (cli/src/monitor.ts). Шаг N считаем
// выполненным наполовину: так полоска не стоит на нуле, пока идёт долгий apt, и не обещает больше, чем известно. // выполненным наполовину: так полоска не стоит на нуле, пока идёт долгий apt, и не обещает больше, чем известно.
const STEP = /^(\d+)\/(\d+)\s+(.+)$/; const STEP = /^(\d+)\/(\d+)\s+(.+)$/;
const SETTLE_MS = 90_000;
function JobProgress({ job, starting }: { job: MonitorStatus['job']; starting: boolean }) { function JobProgress({ job, starting }: { job: MonitorStatus['job']; starting: boolean }) {
let step: [number, number, string] | null = null; let step: [number, number, string] | null = null;
for (const l of job?.log.split('\n') ?? []) { const m = STEP.exec(l.trim()); if (m) step = [Number(m[1]), Number(m[2]), m[3]]; } for (const l of job?.log.split('\n') ?? []) { const m = STEP.exec(l.trim()); if (m) step = [Number(m[1]), Number(m[2]), m[3]]; }
@@ -55,15 +56,22 @@ export default function PrometheusInstall({ token, showMsg }: PageProps) {
useEffect(() => { void loadStatus(); }, [loadStatus]); useEffect(() => { void loadStatus(); }, [loadStatus]);
// Пока идёт установка или удаление — следим за ходом: раз в 2 с читаем статус и лог // Пока идёт установка или удаление — следим за ходом: раз в 2 с читаем статус и лог. Операция
// закончилась, а панель ещё не видит результата (на ноде агент перезапускается и переподключается) —
// следим дальше, пока ready не совпадёт с ней, но не дольше SETTLE_MS: иначе «Готово» висит рядом
// с «не подключён», а пункт «Монитор» ждёт минутного опроса App.
const [settle, setSettle] = useState<number | null>(null); // до какого момента ждём ready
useEffect(() => { useEffect(() => {
if (!pending && !status?.job?.running) return; if (!pending && !status?.job?.running && settle === null) return;
const t = setInterval(async () => { const t = setInterval(async () => {
const s = await loadStatus(); const s = await loadStatus();
if (s && !s.job?.running) setPending(null); if (!s?.job || s.job.running) return; // агент ноды переподключается — job пока не виден
setPending(null);
const waiting = s.job.exit === 0 && s.ready !== (s.job.action === 'install');
setSettle(d => waiting && (d === null || Date.now() < d) ? d ?? Date.now() + SETTLE_MS : null);
}, 2000); }, 2000);
return () => clearInterval(t); return () => clearInterval(t);
}, [pending, status?.job?.running, loadStatus]); }, [pending, status?.job?.running, settle, loadStatus]);
const run = async (action: 'install' | 'remove') => { const run = async (action: 'install' | 'remove') => {
if (action === 'remove' && !confirm('Удалить Prometheus? Он будет остановлен, а накопленные графики (до 31 дня) сотрутся. Панель продолжит отдавать /metrics.')) return; if (action === 'remove' && !confirm('Удалить Prometheus? Он будет остановлен, а накопленные графики (до 31 дня) сотрутся. Панель продолжит отдавать /metrics.')) return;
@@ -75,7 +83,7 @@ export default function PrometheusInstall({ token, showMsg }: PageProps) {
if (!status) return <section className="st-section"><p className="st-hint">Загрузка…</p></section>; if (!status) return <section className="st-section"><p className="st-hint">Загрузка…</p></section>;
const job = status.job; const job = status.job;
const working = !!pending || !!job?.running; const working = !!pending || !!job?.running || settle !== null;
const doing = pending ?? (job?.running ? job.action : null); const doing = pending ?? (job?.running ? job.action : null);
const showJob = !!pending || (!!job && (job.running || job.exit !== null)); const showJob = !!pending || (!!job && (job.running || job.exit !== null));
const failed = !!job && job.exit !== null && job.exit !== 0; const failed = !!job && job.exit !== null && job.exit !== 0;
@@ -96,7 +104,7 @@ export default function PrometheusInstall({ token, showMsg }: PageProps) {
</span> </span>
<span className="st-row-ctl"> <span className="st-row-ctl">
<span className={`chip chip--${status.ready ? 'online' : 'offline'}`}> <span className={`chip chip--${status.ready ? 'online' : 'offline'}`}>
<span className="chip-dot" />{doing === 'install' ? 'ставится' : doing === 'remove' ? 'удаляется' : status.ready ? 'работает' : 'не подключён'} <span className="chip-dot" />{doing === 'install' ? 'ставится' : doing === 'remove' ? 'удаляется' : settle !== null ? (status.ready ? 'отключается' : 'подключается') : status.ready ? 'работает' : 'не подключён'}
</span> </span>
{status.can_install && (status.ready ? ( {status.can_install && (status.ready ? (
<button className="btn btn--danger" onClick={() => run('remove')} disabled={working}> <button className="btn btn--danger" onClick={() => run('remove')} disabled={working}>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "awg-cli", "name": "awg-cli",
"version": "0.5.1", "version": "0.5.2",
"private": true, "private": true,
"dependencies": { "dependencies": {
"tsx": "^4.7.0", "tsx": "^4.7.0",
+1 -1
View File
@@ -188,7 +188,7 @@ trap 'exec >&- 2>&-; wait "$LOG_PID" 2>/dev/null || true; exec 3>&-
# ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает # ⚠️ Дублирует .env из репозитория: установщик собирает URL релиза и печатает
# баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может. # баннер ДО того, как архив с .env скачан, поэтому взять их оттуда не может.
# При бампе версии правь оба места. # При бампе версии правь оба места.
VERSION="0.5.1" VERSION="0.5.2"
CHANNEL="${CHANNEL:-Beta}" CHANNEL="${CHANNEL:-Beta}"
# Откуда брать архив и как называется продукт. По умолчанию — релизы на GitHub; источник # Откуда брать архив и как называется продукт. По умолчанию — релизы на GitHub; источник
+6 -1
View File
@@ -364,9 +364,14 @@ func (m *Monitor) router() *Router {
rt := newRouter() rt := newRouter()
rt.Get("/", func(c *Ctx) bool { rt.Get("/", func(c *Ctx) bool {
src := m.source() src := m.source()
ready := src != nil
target := m.target(src, c.R.URL.Query()) target := m.target(src, c.R.URL.Query())
job := m.jobState(target) job := m.jobState(target)
// Установщик выходит, только когда Prometheus ответил, но проверка выше могла пройти
// до этого: тогда страница получила бы «готово» вместе с ready=false и перестала следить.
if src == nil && !job.running && job.action == "install" && job.exit != nil && jsToNumberAny(job.exit) == 0 {
src = m.source()
}
ready := src != nil
var jobOut any var jobOut any
if job.can && truthy(job.action) { if job.can && truthy(job.action) {
jobOut = jsjson.Object{{Key: "action", Val: job.action}, {Key: "running", Val: job.running}, {Key: "exit", Val: job.exit}, {Key: "log", Val: job.log}} jobOut = jsjson.Object{{Key: "action", Val: job.action}, {Key: "running", Val: job.running}, {Key: "exit", Val: job.exit}, {Key: "log", Val: job.log}}