fix(traffic): показывать причину неполного опроса peers

This commit is contained in:
Denozordec committed 2026-10-12 22:54:35 +07:00
1 parent d8f8d3c7a4
commit c69d99ba80
19 files changed
+1367 -23

No files matched your search

+4 -2
View File
@@ -198,11 +198,12 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
return reply.status(502).send({ ok: false, error, failures, succeeded: result.servers.filter(s => s.ok).map(s => s.serverId) })
}
const updated = await getTrafficSettings()
const warnings = result.servers.filter(s => s.warning).map(s => ({ serverId: s.serverId, warning: s.warning }))
await appendEvent({
level: "info",
level: warnings.length ? "warning" : "info",
eventType: "traffic.collect.manual.ok",
sourceModule: "traffic",
title: "Ручной сбор трафика завершен",
title: warnings.length ? "Ручной сбор трафика завершён с ограничениями" : "Ручной сбор трафика завершен",
message: `Длительность: ${updated.lastDurationMs ?? 0} мс`,
payload: {
lastCollectedAt: updated.lastCollectedAt ?? null,
@@ -210,6 +211,7 @@ const trafficRoutes: FastifyPluginAsyncZod = async (app) => {
})
return reply.send({
ok: true,
warnings,
lastCollectedAt: updated.lastCollectedAt ?? null,
lastDurationMs: updated.lastDurationMs ?? null,
lastError: updated.lastError || null,
+6 -4
View File
@@ -165,6 +165,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
flags: encodeTrafficFlags(running, disabled, true, true),
}
})
let peerWarning: string | undefined
try {
const peers = await client.get<RosWgPeerTraffic[]>("/interface/wireguard/peers")
for (const p of parseTrafficPeers(peers)) {
@@ -189,18 +190,19 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
flags: encodeTrafficFlags(running, disabled, true, true),
})
}
} catch {
/* WG peers optional — iface samples already recorded */
} catch (error) {
peerWarning = `Измерения WireGuard peers недоступны: ${error instanceof Error ? error.message : String(error)}`
}
if (rows.length > 0) {
await db.insert(trafficSamples).values(rows)
}
await recordTrafficCollectionAttempt(srv, "ok", now)
await recordTrafficCollectionAttempt(srv, "ok", now, peerWarning)
snapshot.servers.push({
serverId: srv.id,
name,
host: srv.host,
ok: true,
...(peerWarning ? { warning: peerWarning } : {}),
interfaces: ifaces.length,
...(includedInterfaces > 0 && totalsMeasured ? {
sumRxMbps,
@@ -222,7 +224,7 @@ export async function collectTrafficOnce(): Promise<TrafficRunSnapshot> {
await db.update(trafficSettings).set({
lastCollectedAt: now,
lastDurationMs: Date.now() - startedAt,
lastError: snapshot.servers.filter(s => !s.ok).map(s => `${s.name}: ${s.error ?? "Ошибка опроса"}`).join("; "),
lastError: snapshot.servers.filter(s => !s.ok || s.warning).map(s => `${s.name}: ${s.warning ?? s.error ?? "Ошибка опроса"}`).join("; "),
updatedAt: now,
}).where(eq(trafficSettings.id, 1))
} catch (e) {
@@ -9,8 +9,8 @@ const database=process.env.MMAPP_OWN_SQL_DB
if(!database)console.log("traffic status SQL: skipped; explicit owned database required")
else{
assert(/^mm_accept_166_\d+$/.test(database));assert.equal(new URL(process.env.DATABASE_URL!).pathname,"/"+database);assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name,database)
const prefix="own-status181-"+Date.now()+"-",userId=prefix+"user";let serverId:number|undefined,cases=0,deny=false,flag:unknown=true,peerDisabled=false,peerVariant="valid"
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?(peerVariant==="shape"?{}:peerVariant==="duplicate"?[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false},{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false}]:[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:peerDisabled},...(peerVariant==="partial"?[{interface:"wg1","public-key":"broken",rx:"invalid",tx:"1000",disabled:false}]:[])]):[{name:"ether1",running:flag,disabled:false,"rx-byte":"1000","tx-byte":"1000"}]))})
const prefix="own-status181-"+Date.now()+"-",userId=prefix+"user";let serverId:number|undefined,cases=0,deny=false,flag:unknown=true,peerDisabled=false,peerVariant="valid",denyPeer=false
const http=createServer((req,res)=>{res.setHeader("Content-Type","application/json");if(deny || denyPeer && req.url?.includes("/wireguard/")){res.statusCode=401;return res.end('{"message":"Unauthorized"}')}res.end(JSON.stringify(req.url?.includes("/wireguard/")?(peerVariant==="shape"?{}:peerVariant==="duplicate"?[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false},{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:false}]:[{interface:"wg1","public-key":"fixturekey",rx:"1000",tx:"1000",disabled:peerDisabled},...(peerVariant==="partial"?[{interface:"wg1","public-key":"broken",rx:"invalid",tx:"1000",disabled:false}]:[])]):[{name:"ether1",running:flag,disabled:false,"rx-byte":"1000","tx-byte":"1000"}]))})
await new Promise<void>(resolve=>http.listen(0,"127.0.0.1",resolve));const address=http.address();assert(address&&typeof address==="object");const app=Fastify();await app.register(trafficRoutes)
const bindings=async()=>{const r=await app.inject({method:"GET",url:"/traffic/bound-interfaces?range=1h"});assert.equal(r.statusCode,200);return r.json().interfaces.filter((i:{userId:string})=>i.userId===userId)}
try{
@@ -28,10 +28,13 @@ else{
deny=false;await collectTrafficOnce();await pool.query("UPDATE traffic_samples SET flags=flags & 7 WHERE server_id=$1",[serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
await collectTrafficOnce();await pool.query("UPDATE traffic_collection_state SET last_attempt_at=$1 WHERE server_id=$2",[new Date(Date.now()-100_000).toISOString(),serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
for(const variant of["partial","duplicate","shape"]){
peerVariant=variant;await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);const result=await collectTrafficOnce();assert.equal(result.servers[0].ok,true)
peerVariant=variant;await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);const result=await collectTrafficOnce();assert.equal(result.servers[0].ok,true);assert(result.servers[0].warning?.includes("WireGuard"));assert((await pool.query("SELECT error FROM traffic_collection_state WHERE server_id=$1",[serverId])).rows[0].error.includes("WireGuard"))
const saved=(await pool.query("SELECT peer_public_key FROM traffic_samples WHERE server_id=$1",[serverId])).rows;assert.equal(saved.length,1);assert.equal(saved[0].peer_public_key,"")
rows=await bindings();assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="wg1").status,"unknown");assert.equal(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").status,"online");cases++
}
denyPeer=true;peerVariant="valid";let partial=await collectTrafficOnce();assert.equal(partial.servers[0].ok,true);assert(partial.servers[0].warning?.includes("401"));rows=await bindings();assert(rows.find((r:{interfaceName:string})=>r.interfaceName==="ether1").observations.error.includes("401"));cases++
const manualPartial=await app.inject({method:"POST",url:"/traffic/collect-now"});assert.equal(manualPartial.statusCode,200);assert.equal(manualPartial.json().ok,true);assert.equal(manualPartial.json().warnings.length,1);assert(manualPartial.json().lastError.includes("401"));
denyPeer=false;partial=await collectTrafficOnce();assert.equal(partial.servers[0].warning,undefined);assert.equal((await pool.query("SELECT error FROM traffic_collection_state WHERE server_id=$1",[serverId])).rows[0].error,null);cases++
await pool.query("UPDATE servers SET enabled=false WHERE id=$1",[serverId]);rows=await bindings();assert(rows.every((r:{status:string})=>r.status==="unknown"));cases++
console.log(`traffic status owned SQL: ${cases} cases; first-wave native flags/peer config/invalid/error/legacy/stale/disabled passed`)
}finally{await app.close();await new Promise<void>(resolve=>http.close(()=>resolve()));await pool.query("DELETE FROM app_users WHERE id=$1",[userId]);if(serverId!==undefined){await pool.query("DELETE FROM traffic_samples WHERE server_id=$1",[serverId]);await pool.query("DELETE FROM servers WHERE id=$1",[serverId])}await closePool()}
@@ -11,6 +11,8 @@ export interface TrafficServerSnapshot {
host: string
ok: boolean
error?: string
/** Optional peer slice failed; successful interface measurements are retained. */
warning?: string
/** Сколько интерфейсов записано в сэмплы */
interfaces?: number
/** Сумма RX по интерфейсам, Мбит/с (округл.) */