From b619ef208e0696bbd00f72dedba8ee7f52fc59f4 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Sun, 11 Oct 2026 12:59:57 +0700 Subject: [PATCH] =?UTF-8?q?fix(netflow):=20=D0=B8=D1=81=D0=BA=D0=BB=D1=8E?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20IPv4=20=D0=B8=D0=B7=20=D0=B2?= =?UTF-8?q?=D0=BD=D0=B5=D1=88=D0=BD=D0=B5=D0=B3=D0=BE=20=D0=BE=D0=B1=D0=BE?= =?UTF-8?q?=D0=B3=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/services/statistics-aggregate.test.ts | 1 + .../services/traffic-flow-analytics.test.ts | 86 ++++--- backend/src/services/traffic-flow-ip.test.ts | 14 ++ backend/src/services/traffic-flow-ip.ts | 24 +- .../services/traffic-flow-map-hops.test.ts | 30 +-- .../src/services/traffic-flow-ripe.test.ts | 15 +- docs/ux-ui-external-acceptance.md | 16 +- docs/ux-ui-plan.md | 6 +- docs/ux-ui-progress.md | 16 +- docs/ux-ui-remaining-acceptance.md | 20 +- docs/ux-ui-stage-12.md | 14 +- docs/ux-ui-test-server.md | 16 +- .../browser-flow-special-ip-2026-10-11.json | 55 +++++ .../flow-special-ipv4-2026-10-11.json | 216 ++++++++++++++++++ 14 files changed, 448 insertions(+), 81 deletions(-) create mode 100644 docs/ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json create mode 100644 docs/ux-ui/acceptance/flow-special-ipv4-2026-10-11.json diff --git a/backend/src/services/statistics-aggregate.test.ts b/backend/src/services/statistics-aggregate.test.ts index c2f53778..0656643c 100644 --- a/backend/src/services/statistics-aggregate.test.ts +++ b/backend/src/services/statistics-aggregate.test.ts @@ -84,6 +84,7 @@ if (enId == null) throw new Error("no en server") await ensurePartitionFor(pool, "flow_daily_facts", "month", new Date("2026-09-01T00:00:00Z")) await ensurePartitionFor(pool, "flow_hour_facts", "day", new Date("2026-09-10T00:00:00Z")) +await ensurePartitionFor(pool, "server_snapshots", "week", new Date("2026-09-10T00:00:00Z")) await dbQuery(`DELETE FROM flow_daily_facts WHERE server_id IN ($1, $2)`, [serverId, enId]) await dbQuery(`DELETE FROM flow_hour_facts WHERE server_id IN ($1, $2)`, [serverId, enId]) await dbQuery(`DELETE FROM user_interface_bindings WHERE server_id = $1`, [serverId]) diff --git a/backend/src/services/traffic-flow-analytics.test.ts b/backend/src/services/traffic-flow-analytics.test.ts index af45c2ad..1868afff 100644 --- a/backend/src/services/traffic-flow-analytics.test.ts +++ b/backend/src/services/traffic-flow-analytics.test.ts @@ -44,6 +44,12 @@ if (!(await withPgOrSkip())) { process.exit(0) } +const fixture = await dbQuery<{ id: number }>( + "INSERT INTO servers (name, host) VALUES ($1, $2) RETURNING id", + ["analytics-test-" + Date.now(), "127.0.0.1"], +) +const serverId = fixture.rows[0]!.id +try { disableCatalogFetchForTests() resetFlowCatalogForTests() disableRipePersistForTests() @@ -60,13 +66,13 @@ seedFlowTopologyForTests({ wanIfaces: new Map(), plane: { clientIfaceNames: new Set(), enHosts: new Set(), jhHosts: new Set() }, }) -rememberServerIfaces(7, [ +rememberServerIfaces(serverId, [ { ".id": "*2", name: "ether1" }, { ".id": "*B", name: "ether2" }, { ".id": "*A", name: "wg-flow" }, ]) -ingestParsedFlowsForServerForTests(7, [ +ingestParsedFlowsForServerForTests(serverId, [ { src: "10.1.1.8", dst: "8.8.8.8", @@ -92,7 +98,7 @@ ingestParsedFlowsForServerForTests(7, [ ]) try { - const all = await buildFlowAnalytics({ minutes: 5, serverId: 7 }) + const all = await buildFlowAnalytics({ minutes: 5, serverId }) assert.equal(all.applications[0]?.label, "HTTPS") assert.ok(all.protocols.some((p) => p.label === "TCP")) assert.equal(all.ifaces[0]?.name, "ether1") @@ -104,11 +110,11 @@ try { assert.equal(conv.application, "HTTPS") assert.ok(!/^\d+$/.test(conv.inIface)) - const filtered = await buildFlowAnalytics({ minutes: 5, serverId: 7, iface: "ether1" }) + const filtered = await buildFlowAnalytics({ minutes: 5, serverId, iface: "ether1" }) assert.ok(filtered.bytes >= 12_000) assert.equal(filtered.ifaces[0]?.name, "ether1") - const miss = await buildFlowAnalytics({ minutes: 5, serverId: 7, iface: "wg-flow" }) + const miss = await buildFlowAnalytics({ minutes: 5, serverId, iface: "wg-flow" }) assert.equal(miss.conversations, 0) const other = await buildFlowAnalytics({ minutes: 5, serverId: 99 }) @@ -120,12 +126,12 @@ try { resetFlowRingsForTests() resetIfaceCacheForTests() -rememberServerIfaces(7, [ +rememberServerIfaces(serverId, [ { ".id": "*2", name: "ether1" }, { ".id": "*B", name: "ether2" }, { ".id": "*A", name: "wg-flow" }, ]) -ingestParsedFlowsForServerForTests(7, [ +ingestParsedFlowsForServerForTests(serverId, [ { src: "10.1.1.8", dst: "8.8.8.8", @@ -150,10 +156,10 @@ ingestParsedFlowsForServerForTests(7, [ }, ]) try { - const summed = await buildFlowAnalytics({ minutes: 5, serverId: 7, dedup: false }) + const summed = await buildFlowAnalytics({ minutes: 5, serverId, dedup: false }) assert.equal(summed.bytes, 21_000) assert.equal(summed.conversations, 2) - const deduped = await buildFlowAnalytics({ minutes: 5, serverId: 7, dedup: true }) + const deduped = await buildFlowAnalytics({ minutes: 5, serverId, dedup: true }) assert.equal(deduped.bytes, 12_000) assert.equal(deduped.conversations, 1) assert.equal(deduped.dedupApplied, true) @@ -179,8 +185,8 @@ seedRipeCacheForTests({ seedFlowCatalogForTests({ cidrs: [{ cidr: "8.8.8.0/24", purpose: "steam-gaming" }], }) -rememberServerIfaces(7, [{ ".id": "*2", name: "ether1" }]) -ingestParsedFlowsForServerForTests(7, [ +rememberServerIfaces(serverId, [{ ".id": "*2", name: "ether1" }]) +ingestParsedFlowsForServerForTests(serverId, [ { src: "10.1.1.8", dst: "8.8.8.8", @@ -194,7 +200,7 @@ ingestParsedFlowsForServerForTests(7, [ }, ]) try { - const geo = await buildFlowAnalytics({ minutes: 5, serverId: 7 }) + const geo = await buildFlowAnalytics({ minutes: 5, serverId }) assert.equal(geo.categories?.[0]?.label, "Игры") assert.ok(geo.asns?.some((r) => r.label.includes("AS15169"))) assert.equal(geo.countries?.[0]?.id, "US") @@ -224,8 +230,8 @@ seedRipeCacheForTests({ ok: true, fetchedAt: Date.now(), }) -rememberServerIfaces(7, [{ ".id": "*2", name: "ether1" }]) -ingestParsedFlowsForServerForTests(7, [ +rememberServerIfaces(serverId, [{ ".id": "*2", name: "ether1" }]) +ingestParsedFlowsForServerForTests(serverId, [ { src: "10.1.1.8", dst: "1.1.1.1", @@ -239,7 +245,7 @@ ingestParsedFlowsForServerForTests(7, [ }, ]) try { - const cf = await buildFlowAnalytics({ minutes: 5, serverId: 7 }) + const cf = await buildFlowAnalytics({ minutes: 5, serverId }) assert.equal(cf.countries?.[0]?.id, "US") assert.ok(cf.mapEdges?.every((e) => e.toCountry !== "?")) assert.equal(cf.services?.[0]?.label, "Cloudflare") @@ -254,7 +260,7 @@ try { } { - ingestParsedFlowsForServerForTests(7, [ + ingestParsedFlowsForServerForTests(serverId, [ { src: "10.1.1.8", dst: "8.8.8.8", @@ -267,7 +273,7 @@ try { outIface: "", }, ]) - const degraded = await buildFlowAnalytics({ minutes: 5, serverId: 7, skipHeavy: true }) + const degraded = await buildFlowAnalytics({ minutes: 5, serverId, skipHeavy: true }) assert.equal(degraded.degraded, true) assert.equal(degraded.conversationsList.length, 0) assert.ok((degraded.bytes ?? 0) >= 12_000) @@ -283,22 +289,22 @@ try { const { pool } = await import("../db/index.js") const { ensurePartitionFor } = await import("../db/partitions.js") await ensurePartitionFor(pool, "flow_daily_dims", "month", new Date("2026-09-01T00:00:00Z")) - await dbQuery(`DELETE FROM flow_daily_dims WHERE server_id = 7 AND day >= '2026-09-01' AND day < '2026-10-01'`) + await dbQuery(`DELETE FROM flow_daily_dims WHERE server_id = $1 AND day >= '2026-09-01' AND day < '2026-10-01'`, [serverId]) await dbQuery(` INSERT INTO flow_daily_dims (server_id, day, dim, key, bytes, packets) VALUES - (7, '2026-09-01', 'country', 'US', 1000, 10), - (7, '2026-09-02', 'country', 'US', 500, 5), - (7, '2026-09-01', 'service', 'steam', 800, 8), - (7, '2026-09-01', 'asn', '15169', 900, 9), - (7, '2026-09-01', 'asn', 'other', 100, 1) - `) - const monthly = await getFlowMonthly("2026-09", 7) + ($1, '2026-09-01', 'country', 'US', 1000, 10), + ($1, '2026-09-02', 'country', 'US', 500, 5), + ($1, '2026-09-01', 'service', 'steam', 800, 8), + ($1, '2026-09-01', 'asn', '15169', 900, 9), + ($1, '2026-09-01', 'asn', 'other', 100, 1) + `, [serverId]) + const monthly = await getFlowMonthly("2026-09", serverId) assert.equal(monthly.bytes, 1500) assert.equal(monthly.countries[0]?.id, "US") assert.equal(monthly.countries[0]?.bytes, 1500) assert.ok(monthly.asns.some((row) => row.id === "other")) - await dbQuery(`DELETE FROM flow_daily_dims WHERE server_id = 7 AND day >= '2026-09-01' AND day < '2026-10-01'`) + await dbQuery(`DELETE FROM flow_daily_dims WHERE server_id = $1 AND day >= '2026-09-01' AND day < '2026-10-01'`, [serverId]) } } @@ -308,18 +314,18 @@ try { resetRipeCacheForTests() resetFlowCatalogForTests() disableRipeEnqueueForTests() - rememberServerIfaces(7, [ + rememberServerIfaces(serverId, [ { ".id": "*2", name: "gre-client" }, { ".id": "*3", name: "NSK-SERVHOST-RTK" }, { ".id": "*4", name: "gre-en-nsk" }, ]) const topo: FlowTopology = { - clientIfaces: new Map([[7, new Set(["gre-client"])]]), - clientByIface: new Map([["7|gre-client", { + clientIfaces: new Map([[serverId, new Set(["gre-client"])]]), + clientByIface: new Map([[serverId + "|gre-client", { userId: "u1", login: "alice", name: "Alice", - serverId: 7, + serverId, interfaceName: "gre-client", }]]), enNodes: [{ id: 9, name: "NSK-SERVHOST-RTK", hosts: ["198.51.100.1"] }], @@ -343,7 +349,7 @@ try { ok: true, fetchedAt: Date.now(), }) - ingestParsedFlowsForServerForTests(7, [ + ingestParsedFlowsForServerForTests(serverId, [ { src: "10.100.1.17", dst: "173.194.160.163", @@ -380,7 +386,7 @@ try { }, ]) try { - const def = await buildFlowAnalytics({ minutes: 5, serverId: 7 }) + const def = await buildFlowAnalytics({ minutes: 5, serverId }) assert.equal(def.bytes, 12_000) assert.equal(def.bytesPayload, 12_000) assert.equal(def.bytesOverlay, 5_000_000) @@ -398,7 +404,7 @@ try { assert.equal(path.clientName, "Alice") assert.equal(path.enName, "NSK-SERVHOST-RTK") assert.equal(path.dst, "173.194.160.163") - const withAll = await buildFlowAnalytics({ minutes: 5, serverId: 7, excludeOverlay: false, excludeMesh: false }) + const withAll = await buildFlowAnalytics({ minutes: 5, serverId, excludeOverlay: false, excludeMesh: false }) assert.equal(withAll.bytes, 12_000 + 5_000_000 + 8000) assert.ok(withAll.conversationsList.some((r) => r.plane === "overlay")) assert.ok(withAll.conversationsList.some((r) => r.plane === "client_mesh")) @@ -446,8 +452,8 @@ try { ok: true, fetchedAt: Date.now(), }) - rememberServerIfaces(7, [{ ".id": "*2", name: "ether1" }]) - ingestParsedFlowsForServerForTests(7, [ + rememberServerIfaces(serverId, [{ ".id": "*2", name: "ether1" }]) + ingestParsedFlowsForServerForTests(serverId, [ { src: "74.125.104.196/32", dst: "10.200.100.53/32", @@ -483,7 +489,7 @@ try { }, ]) try { - const rev = await buildFlowAnalytics({ minutes: 5, serverId: 7 }) + const rev = await buildFlowAnalytics({ minutes: 5, serverId }) const google = rev.conversationsList.find((r) => r.src === "74.125.104.196") const cf = rev.conversationsList.find((r) => r.src === "104.18.35.51") const fastly = rev.conversationsList.find((r) => r.src === "146.75.118.132") @@ -515,7 +521,7 @@ try { if (await withPgOrSkip()) { resetFlowRingsForTests() resetIfaceCacheForTests() - const sidRow = await dbQuery<{ id: number }>(`SELECT id FROM servers LIMIT 1`) + const sidRow = await dbQuery<{ id: number }>(`SELECT id FROM servers WHERE id = $1`, [serverId]) const sid = sidRow.rows[0]?.id if (sid) { const { pool } = await import("../db/index.js") @@ -550,4 +556,10 @@ try { } } +} finally { + for (const table of ["flow_daily_dims", "flow_hour_facts", "flow_daily_facts", "flow_buckets", "traffic_samples", "user_interface_bindings", "server_snapshots"]) { + await dbQuery("DELETE FROM " + table + " WHERE server_id = $1", [serverId]) + } + await dbQuery("DELETE FROM servers WHERE id = $1", [serverId]) +} console.log("traffic-flow-analytics.test.ts: ok") diff --git a/backend/src/services/traffic-flow-ip.test.ts b/backend/src/services/traffic-flow-ip.test.ts index c1e12500..48eb57c2 100644 --- a/backend/src/services/traffic-flow-ip.test.ts +++ b/backend/src/services/traffic-flow-ip.test.ts @@ -18,6 +18,20 @@ assert.equal(isNonPublicIp("10.200.100.53/32"), true) assert.equal(isNonPublicIp("173.194.151.65"), false) assert.equal(isNonPublicIp("74.125.104.196/32"), false, "PG inet::text не делает Google приватным") +for (const ip of [ + "198.18.0.0", "198.19.255.255", "::ffff:198.18.139.1/32", + "192.0.0.0", "192.0.0.8", "192.0.0.11", "192.0.0.255", + "192.0.2.0", "192.0.2.255", "198.51.100.0", "198.51.100.255", + "203.0.113.0", "203.0.113.255", "192.88.99.2", + "240.0.0.0", "254.255.255.255", "255.255.255.255", +]) assert.equal(isNonPublicIp(ip), true, ip) +for (const ip of [ + "198.17.255.255", "198.20.0.0", "192.0.0.9", "192.0.0.10/32", + "192.0.1.255", "192.0.3.0", "198.51.99.255", "198.51.101.0", + "203.0.112.255", "203.0.114.0", "192.31.196.1", "192.52.193.1", + "192.175.48.1", "223.255.255.255", +]) assert.equal(isNonPublicIp(ip), false, ip) + assert.equal( pickInternetPeer("173.194.151.65", "10.200.100.53", 443, 57182), "173.194.151.65", diff --git a/backend/src/services/traffic-flow-ip.ts b/backend/src/services/traffic-flow-ip.ts index 8204b87b..3b510f9d 100644 --- a/backend/src/services/traffic-flow-ip.ts +++ b/backend/src/services/traffic-flow-ip.ts @@ -48,6 +48,16 @@ export function ipInCidrV4(ip: string, cidr: string): boolean { return ((addr & parsed.mask) >>> 0) === parsed.net } +// IPv4 ranges not globally reachable in the IANA special-purpose registry. +// Parse fixed ranges once: this predicate also runs for each flow endpoint. +const nonPublicV4Ranges = [ + "0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", + "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", + "192.88.99.2/32", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", + "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4", +].map(cidr => parseCidrV4(cidr)!) +const publicProtocolAnycastV4 = new Set([0xc0000009, 0xc000000a]) // 192.0.0.9/10 + export function isNonPublicIp(ip: string): boolean { const trimmed = canonicalIp(ip) if (!trimmed) return true @@ -57,18 +67,8 @@ export function isNonPublicIp(ip: string): boolean { } const n = ipv4ToInt(trimmed) if (n == null) return true - const inRange = (cidr: string) => ipInCidrV4(trimmed, cidr) - return ( - inRange("0.0.0.0/8") - || inRange("10.0.0.0/8") - || inRange("127.0.0.0/8") - || inRange("169.254.0.0/16") - || inRange("172.16.0.0/12") - || inRange("192.168.0.0/16") - || inRange("100.64.0.0/10") - || inRange("224.0.0.0/4") - || inRange("255.255.255.255/32") - ) + if (publicProtocolAnycastV4.has(n)) return false + return nonPublicV4Ranges.some(({ net, mask }) => ((n & mask) >>> 0) === net) } const PEER_WELL_KNOWN_PORTS = new Set([80, 443, 53, 853]) diff --git a/backend/src/services/traffic-flow-map-hops.test.ts b/backend/src/services/traffic-flow-map-hops.test.ts index 0b1feabf..1792d9dc 100644 --- a/backend/src/services/traffic-flow-map-hops.test.ts +++ b/backend/src/services/traffic-flow-map-hops.test.ts @@ -290,7 +290,7 @@ rememberServerIfaces(7, [ googleRipe() ingestParsedFlowsForServerForTests(7, [ payloadFlow("8.8.8.8", 600), - payloadFlow("203.0.113.50", 9400), + payloadFlow("11.0.113.50", 9400), ]) try { resetFlowMapHopsCacheForTests() @@ -335,7 +335,7 @@ rememberServerIfaces(7, [ googleRipe() ingestParsedFlowsForServerForTests(7, [ payloadFlow("8.8.8.8", 400), - payloadFlow("203.0.113.50", 9600), + payloadFlow("11.0.113.50", 9600), ]) try { resetFlowMapHopsCacheForTests() @@ -370,7 +370,7 @@ rememberServerIfaces(7, [ ingestParsedFlowsForServerForTests(7, [ payloadFlow("8.8.8.8", 400), payloadFlow("104.18.35.51", 400), - payloadFlow("203.0.113.50", 9200), + payloadFlow("11.0.113.50", 9200), ]) try { resetFlowMapHopsCacheForTests() @@ -424,7 +424,7 @@ rememberServerIfaces(7, [ ]) ingestParsedFlowsForServerForTests(7, [ payloadFlow("64.233.161.1", 10_000), - payloadFlow("203.0.113.50", 20_000), + payloadFlow("11.0.113.50", 20_000), ]) try { resetFlowMapHopsCacheForTests() @@ -451,15 +451,15 @@ try { } const smallBrands: Array<{ ip: string; asn: number; holder: string; bytes: number; id: string }> = [ - { ip: "203.0.113.1", asn: 714, holder: "APPLE-ENGINEERING", bytes: 400, id: "svc:apple" }, - { ip: "203.0.113.2", asn: 36459, holder: "GITHUB", bytes: 390, id: "svc:github" }, - { ip: "203.0.113.3", asn: 54876, holder: "GITLAB", bytes: 380, id: "svc:gitlab" }, - { ip: "203.0.113.4", asn: 8403, holder: "SPOTIFY", bytes: 370, id: "svc:spotify" }, - { ip: "203.0.113.5", asn: 13414, holder: "TWITTER", bytes: 360, id: "svc:x" }, - { ip: "203.0.113.6", asn: 47541, holder: "VKONTAKTE", bytes: 350, id: "svc:vk" }, - { ip: "203.0.113.7", asn: 30103, holder: "ZOOM", bytes: 340, id: "svc:zoom" }, - { ip: "203.0.113.8", asn: 395701, holder: "EPIC-GAMES", bytes: 330, id: "svc:epic" }, - { ip: "203.0.113.9", asn: 6507, holder: "RIOT-GAMES", bytes: 320, id: "svc:riot" }, + { ip: "11.0.113.1", asn: 714, holder: "APPLE-ENGINEERING", bytes: 400, id: "svc:apple" }, + { ip: "11.0.113.2", asn: 36459, holder: "GITHUB", bytes: 390, id: "svc:github" }, + { ip: "11.0.113.3", asn: 54876, holder: "GITLAB", bytes: 380, id: "svc:gitlab" }, + { ip: "11.0.113.4", asn: 8403, holder: "SPOTIFY", bytes: 370, id: "svc:spotify" }, + { ip: "11.0.113.5", asn: 13414, holder: "TWITTER", bytes: 360, id: "svc:x" }, + { ip: "11.0.113.6", asn: 47541, holder: "VKONTAKTE", bytes: 350, id: "svc:vk" }, + { ip: "11.0.113.7", asn: 30103, holder: "ZOOM", bytes: 340, id: "svc:zoom" }, + { ip: "11.0.113.8", asn: 395701, holder: "EPIC-GAMES", bytes: 330, id: "svc:epic" }, + { ip: "11.0.113.9", asn: 6507, holder: "RIOT-GAMES", bytes: 320, id: "svc:riot" }, ] resetFlowRingsForTests() @@ -513,7 +513,7 @@ ingestParsedFlowsForServerForTests(7, [ inIface: "3", outIface: "3", }, - payloadFlow("203.0.113.50", 1000), + payloadFlow("11.0.113.50", 1000), ]) try { resetFlowMapHopsCacheForTests() @@ -880,7 +880,7 @@ rememberServerIfaces(7, [ googleRipe() ingestParsedFlowsForServerForTests(7, [ payloadFlow("8.8.8.8", 600), - payloadFlow("203.0.113.50", 9400), + payloadFlow("11.0.113.50", 9400), ]) try { resetFlowMapHopsCacheForTests() diff --git a/backend/src/services/traffic-flow-ripe.test.ts b/backend/src/services/traffic-flow-ripe.test.ts index 744bf832..2fe8852e 100644 --- a/backend/src/services/traffic-flow-ripe.test.ts +++ b/backend/src/services/traffic-flow-ripe.test.ts @@ -58,17 +58,26 @@ enqueueRipeMisses(["8.8.8.10"]) await flushRipeQueueForTests() assert.equal(ripeFetchCountForTests(), afterFirst) +seedRipeCacheForTests({ prefix: "198.18.0.0/15", asn: 64500, country: "NL", lat: 52, lng: 4, holder: "STALE", ok: true, fetchedAt: Date.now() }) +for (const ip of ["198.18.139.1", "198.19.255.255", "192.0.2.1", "198.51.100.1", "203.0.113.1", "240.0.0.1"]) { + assert.equal(lookupRipeCached(ip)?.ok, false, "Special ranges must bypass stale public cache") +} +enqueueRipeMisses(["198.18.139.1", "198.19.255.255", "192.0.2.1", "198.51.100.1", "203.0.113.1", "240.0.0.1"]) +await flushRipeQueueForTests() +assert.equal(ripeFetchCountForTests(), afterFirst, "Special ranges must never query RIPE") + resetRipeCacheForTests() disableRipePersistForTests() setRipeFetchForTests(async () => { throw new Error("timeout") }) -enqueueRipeMisses(["203.0.113.50"]) +enqueueRipeMisses(["8.9.8.50"]) await flushRipeQueueForTests() -const neg = lookupRipeCached("203.0.113.50") +const neg = lookupRipeCached("8.9.8.50") assert.equal(neg?.ok, false) const afterNeg = ripeFetchCountForTests() -enqueueRipeMisses(["203.0.113.50"]) +assert.ok(afterNeg > 0, "Upstream failure test must actually query a public address") +enqueueRipeMisses(["8.9.8.50"]) await flushRipeQueueForTests() assert.equal(ripeFetchCountForTests(), afterNeg) diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 79c652f4..d57fcecb 100644 --- a/docs/ux-ui-external-acceptance.md +++ b/docs/ux-ui-external-acceptance.md @@ -1,6 +1,6 @@ # UX/UI: протокол внешней приёмки 12.5 -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md). @@ -696,7 +696,7 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). @@ -1499,3 +1499,15 @@ Own issuer chain удалена только после доказательст Реальный production RIPE service в отдельном процессе, без чтения/записи рабочего кеша, обогатил194.226.121.150 и104.165.244.7: network-info/maxmind-geo-lite/as-overview200, prefix/ASN/holder/country/coordinates получены. Это provider metadata, не гарантия физического положения хоста и не complete NetFlow pipeline. Дополнительный no-fetch контроль с198.18.139.1 failed: benchmark IPv4 ошибочно отправлен upstream. Private192.168/loopback контроль не вызвал запросов; public cache hits проверяются отдельно в следующем блоке. Этот отказ сохранён, весь RIPE test не объявлен success. [Регрессия, compiled hashes и partial RIPE](ux-ui/acceptance/regression-runtime-ripe-2026-10-11.json). Перед регрессией сохранены586 existing evidence files.125 автоматически изменённых файлов восстановлены; все исходные SHA256 совпали, включая восемь pre-existing dirty GRE/VXLAN PNG. Новые135–138 evidence не затёрты. Подробные временные logs/snapshots в ignored tmp; побочные generated screenshots не включены в коммит. Source/runtime не менялись; backend/frontend ux-log133. Следующий140 — исправление фильтра специальных IPv4 согласно IANA, профильные backend/flow проверки, build/deploy и повторный live RIPE.12.5 открыт. + +## 12.5.140 — специальные IPv4, RIPE и изолированная SQL-приёмка (2026-10-11) + +Исправлен isNonPublicIp: benchmark198.18/15, TEST-NET ranges, reserved240/4 и non-global IETF assignments исключены из внешнего RIPE lookup. Глобальные PCP/TURN anycast192.0.0.9/10 сохранены eligible. Фиксированные CIDR masks вычисляются один раз; hot flow predicate больше не разбирает строки CIDR/адреса для каждого range. IPFIX tuples, NAT, network config и DB schema не менялись.31 boundary/exception checks и stale public-cache bypass/no-fetch тесты passed; [IANA](https://www.iana.org/assignments/iana-ipv4-special-registry/). + +Local test:traffic-flow (20 scripts) exit0, но шесть SQL modules включали PG skips — не объявлены pass. Все шесть затем проверены against production candidate в четырёх собственных temporary PostgreSQL DBs. Parent создаёт exact own DB, child проверяет current_database до test import, DATABASE_PATH/GEOIP_DIR вынесены в ephemeral container; original DB/legacy SQLite не использовались fixtures. Все четыре DB удалены с owner/prefix check, включая failed runs. + +Initial failures сохранены и исправлены в tests: analytics зависел от чужого hardcoded server7 (FK); теперь создаётся own server, все SQL scoped, finally cleanup. Map-hops использовал TEST-NET как вымышленные публичные сервисы; fixtures получили eligible synthetic IPv4, assertions не ослаблены. Statistics не создавал historical server_snapshots partition; теперь создаёт exact fixed-date partition. Ingest/analytics/map-hops/purge/facts-rebuild/statistics SQL modules passed across bounded runs; прошедшие modules без необходимости не повторялись. Mock upstream failure fixture перенесён с TEST-NET на eligible address и явно подтверждает реальный вызов stub. + +Changed-file ESLint, backend production build, RIPE/IP profile tests passed. Реальный production RIPE service снова обогатил два публичных IP; повторный public cache hit и special/private controls дают0 дополнительных requests. Действует backend ux-ripe140/frontend ux-log133; backend/PG healthy. Все984 emitted outputs совпали с candidate Docker, actual deployed IP service SHA256 совпал; private rollback env/old image сохранены. [Полные попытки/проверки/границы](ux-ui/acceptance/flow-special-ipv4-2026-10-11.json). + +Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. При этом traffic отправил initial requests /api/traffic/servers/srv1 и /interfaces, оба400/id is required; это existing mock-id hydration/catalog gate defect, не часть IPv4 fix и не скрытый success. [Smoke](ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json). Следующий141 — gate live fetch до подтверждённого numeric server id/catalog. IPv6 special ranges — отдельно. Full UI1465 accepted139 до backend-only140; backend/SQL/live delta проверены отдельно, frontend не менялся.12.5 открыт. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 59d7254c..99a77629 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,12 +1,12 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-10. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. ## Актуальная точка продолжения — 2026-10-10 -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. Этапы0–11 завершены в локальных границах журнала. История выполненных внешних блоков и доказательства сохранены в [журнале](ux-ui-stage-12.md) и [протоколе](ux-ui-external-acceptance.md). Реальная Telegram доставка, Portal SSO, multi-instance и оставшиеся профильные сетевые ограничения открыты. @@ -371,6 +371,6 @@ flowchart LR ## 12.5.77 — основной блок IPsec (2026-10-09) -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. Подробный объём, протоколы, схема и ограничения: [12.5.77 в журнале](ux-ui-progress.md). diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index 20385c07..03ee1801 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -36,7 +36,19 @@ | 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 | | 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 | | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | -| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–139 и ограничения ниже. Runtime backend/frontend ux-log133. | +| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–140 и ограничения ниже. Runtime backend ux-ripe140/frontend ux-log133. | + +## 12.5.140 — специальные IPv4, RIPE и изолированная SQL-приёмка (2026-10-11) + +Исправлен isNonPublicIp: benchmark198.18/15, TEST-NET ranges, reserved240/4 и non-global IETF assignments исключены из внешнего RIPE lookup. Глобальные PCP/TURN anycast192.0.0.9/10 сохранены eligible. Фиксированные CIDR masks вычисляются один раз; hot flow predicate больше не разбирает строки CIDR/адреса для каждого range. IPFIX tuples, NAT, network config и DB schema не менялись.31 boundary/exception checks и stale public-cache bypass/no-fetch тесты passed; [IANA](https://www.iana.org/assignments/iana-ipv4-special-registry/). + +Local test:traffic-flow (20 scripts) exit0, но шесть SQL modules включали PG skips — не объявлены pass. Все шесть затем проверены against production candidate в четырёх собственных temporary PostgreSQL DBs. Parent создаёт exact own DB, child проверяет current_database до test import, DATABASE_PATH/GEOIP_DIR вынесены в ephemeral container; original DB/legacy SQLite не использовались fixtures. Все четыре DB удалены с owner/prefix check, включая failed runs. + +Initial failures сохранены и исправлены в tests: analytics зависел от чужого hardcoded server7 (FK); теперь создаётся own server, все SQL scoped, finally cleanup. Map-hops использовал TEST-NET как вымышленные публичные сервисы; fixtures получили eligible synthetic IPv4, assertions не ослаблены. Statistics не создавал historical server_snapshots partition; теперь создаёт exact fixed-date partition. Ingest/analytics/map-hops/purge/facts-rebuild/statistics SQL modules passed across bounded runs; прошедшие modules без необходимости не повторялись. Mock upstream failure fixture перенесён с TEST-NET на eligible address и явно подтверждает реальный вызов stub. + +Changed-file ESLint, backend production build, RIPE/IP profile tests passed. Реальный production RIPE service снова обогатил два публичных IP; повторный public cache hit и special/private controls дают0 дополнительных requests. Действует backend ux-ripe140/frontend ux-log133; backend/PG healthy. Все984 emitted outputs совпали с candidate Docker, actual deployed IP service SHA256 совпал; private rollback env/old image сохранены. [Полные попытки/проверки/границы](ux-ui/acceptance/flow-special-ipv4-2026-10-11.json). + +Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. При этом traffic отправил initial requests /api/traffic/servers/srv1 и /interfaces, оба400/id is required; это existing mock-id hydration/catalog gate defect, не часть IPv4 fix и не скрытый success. [Smoke](ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json). Следующий141 — gate live fetch до подтверждённого numeric server id/catalog. IPv6 special ranges — отдельно. Full UI1465 accepted139 до backend-only140; backend/SQL/live delta проверены отдельно, frontend не менялся.12.5 открыт. ## 12.5.139 — общая регрессия, сверка Docker и реальный RIPE (2026-10-11) diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index fcc00ef9..19571c8c 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,8 +1,8 @@ # Оставшаяся внешняя приёмка — 2026-10-11 -Это актуальная матрица после 12.5.139. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.140. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-log133`, frontend `ux-log133`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-ripe140`, frontend `ux-log133`; production lab использует AUTH_REQUIRED=false. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. | Раздел | Подтверждено | Осталось / условие завершения | | --- | --- | --- | @@ -17,8 +17,8 @@ | BGP | Established83, exact /32 exchange/API/UI/administrative withdrawal92, BFD primary GRE loss/alternate/recovery93 | Hold timer/TCP loss безBFD принят104; packet loss в переходе, разные внешние peers/WAN policy, IPv6/scale. Два логических пути используют тот же public WAN; physical redundancy не принята | | Recursive routes | Production PUT/change/own history restore88, disabled guards, partial/unknown/applied outcomes, live формы/контекст | Автоматический rollback/retry, durable receipts и несколько экземпляров отсутствуют; PUT не является idempotent noop. Полная произвольная legacy catalog миграция не принята | | Diagnostics/terminal | Allowlist/native reads, Ping/trace/route/MTU/DNS, actual avg-rtt, disabled guards, JWT/live PC/mobile89; real default memory tail/log limits/API/UI133 | DNS использует backend resolver; MTU — ограниченная оценка, не гарантированный максимум. Stop ожидания не доказывает остановку RouterOS команды; arbitrary CLI и physical disconnect не приняты | -| Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; внешние RIPE/MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения | -| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | SDK special IPv4 guard fix140 и его профильная проверка/deployment; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | +| Portal/интеграции | Локальные и isolated JWT guards; UI/API error states в границах журналов | Настоящий Portal issuer/login/отзыв прав; RIPE SDK two-IP/no-fetch passed140, полный flow pipeline/local licensed MaxMind/S3/container/certificate jobs и Gitea delivery требуют отдельных тестовых endpoints/аккаунтов. Не отправлять сообщения и не публиковать релиз без соответствующего поручения | +| Общая приёмка | Локальная12.3.83; UI1244/context221/compiled984 parity139; профильные build/E2E/live проверки последующих блоков | Traffic initial live mock-id gate141; IPv6 special ranges; последующие изменения проверять отдельно; scoped axe не означает полную WCAG сертификацию | Доказательства и точные ограничения: [полная история этапа12](ux-ui-stage-12.md), [внешний протокол](ux-ui-external-acceptance.md), [BGP93](ux-ui/acceptance/bgp-failover-2026-10-10.json), [паспорт стенда](ux-ui-test-server.md). Невыполненные пункты не являются все блокерами данного лабораторного сценария: для выпуска необходимо явно определить требуемые конфигурации и принять их либо согласовать ограничение. @@ -414,3 +414,15 @@ Own issuer chain удалена только после доказательст Реальный production RIPE service в отдельном процессе, без чтения/записи рабочего кеша, обогатил194.226.121.150 и104.165.244.7: network-info/maxmind-geo-lite/as-overview200, prefix/ASN/holder/country/coordinates получены. Это provider metadata, не гарантия физического положения хоста и не complete NetFlow pipeline. Дополнительный no-fetch контроль с198.18.139.1 failed: benchmark IPv4 ошибочно отправлен upstream. Private192.168/loopback контроль не вызвал запросов; public cache hits проверяются отдельно в следующем блоке. Этот отказ сохранён, весь RIPE test не объявлен success. [Регрессия, compiled hashes и partial RIPE](ux-ui/acceptance/regression-runtime-ripe-2026-10-11.json). Перед регрессией сохранены586 existing evidence files.125 автоматически изменённых файлов восстановлены; все исходные SHA256 совпали, включая восемь pre-existing dirty GRE/VXLAN PNG. Новые135–138 evidence не затёрты. Подробные временные logs/snapshots в ignored tmp; побочные generated screenshots не включены в коммит. Source/runtime не менялись; backend/frontend ux-log133. Следующий140 — исправление фильтра специальных IPv4 согласно IANA, профильные backend/flow проверки, build/deploy и повторный live RIPE.12.5 открыт. + +## 12.5.140 — специальные IPv4, RIPE и изолированная SQL-приёмка (2026-10-11) + +Исправлен isNonPublicIp: benchmark198.18/15, TEST-NET ranges, reserved240/4 и non-global IETF assignments исключены из внешнего RIPE lookup. Глобальные PCP/TURN anycast192.0.0.9/10 сохранены eligible. Фиксированные CIDR masks вычисляются один раз; hot flow predicate больше не разбирает строки CIDR/адреса для каждого range. IPFIX tuples, NAT, network config и DB schema не менялись.31 boundary/exception checks и stale public-cache bypass/no-fetch тесты passed; [IANA](https://www.iana.org/assignments/iana-ipv4-special-registry/). + +Local test:traffic-flow (20 scripts) exit0, но шесть SQL modules включали PG skips — не объявлены pass. Все шесть затем проверены against production candidate в четырёх собственных temporary PostgreSQL DBs. Parent создаёт exact own DB, child проверяет current_database до test import, DATABASE_PATH/GEOIP_DIR вынесены в ephemeral container; original DB/legacy SQLite не использовались fixtures. Все четыре DB удалены с owner/prefix check, включая failed runs. + +Initial failures сохранены и исправлены в tests: analytics зависел от чужого hardcoded server7 (FK); теперь создаётся own server, все SQL scoped, finally cleanup. Map-hops использовал TEST-NET как вымышленные публичные сервисы; fixtures получили eligible synthetic IPv4, assertions не ослаблены. Statistics не создавал historical server_snapshots partition; теперь создаёт exact fixed-date partition. Ingest/analytics/map-hops/purge/facts-rebuild/statistics SQL modules passed across bounded runs; прошедшие modules без необходимости не повторялись. Mock upstream failure fixture перенесён с TEST-NET на eligible address и явно подтверждает реальный вызов stub. + +Changed-file ESLint, backend production build, RIPE/IP profile tests passed. Реальный production RIPE service снова обогатил два публичных IP; повторный public cache hit и special/private controls дают0 дополнительных requests. Действует backend ux-ripe140/frontend ux-log133; backend/PG healthy. Все984 emitted outputs совпали с candidate Docker, actual deployed IP service SHA256 совпал; private rollback env/old image сохранены. [Полные попытки/проверки/границы](ux-ui/acceptance/flow-special-ipv4-2026-10-11.json). + +Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. При этом traffic отправил initial requests /api/traffic/servers/srv1 и /interfaces, оба400/id is required; это existing mock-id hydration/catalog gate defect, не часть IPv4 fix и не скрытый success. [Smoke](ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json). Следующий141 — gate live fetch до подтверждённого numeric server id/catalog. IPv6 special ranges — отдельно. Full UI1465 accepted139 до backend-only140; backend/SQL/live delta проверены отдельно, frontend не менялся.12.5 открыт. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index ec4d8fbe..87960749 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-11: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Актуальное продолжение 2026-10-11: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -2804,3 +2804,15 @@ Own issuer chain удалена только после доказательст Реальный production RIPE service в отдельном процессе, без чтения/записи рабочего кеша, обогатил194.226.121.150 и104.165.244.7: network-info/maxmind-geo-lite/as-overview200, prefix/ASN/holder/country/coordinates получены. Это provider metadata, не гарантия физического положения хоста и не complete NetFlow pipeline. Дополнительный no-fetch контроль с198.18.139.1 failed: benchmark IPv4 ошибочно отправлен upstream. Private192.168/loopback контроль не вызвал запросов; public cache hits проверяются отдельно в следующем блоке. Этот отказ сохранён, весь RIPE test не объявлен success. [Регрессия, compiled hashes и partial RIPE](ux-ui/acceptance/regression-runtime-ripe-2026-10-11.json). Перед регрессией сохранены586 existing evidence files.125 автоматически изменённых файлов восстановлены; все исходные SHA256 совпали, включая восемь pre-existing dirty GRE/VXLAN PNG. Новые135–138 evidence не затёрты. Подробные временные logs/snapshots в ignored tmp; побочные generated screenshots не включены в коммит. Source/runtime не менялись; backend/frontend ux-log133. Следующий140 — исправление фильтра специальных IPv4 согласно IANA, профильные backend/flow проверки, build/deploy и повторный live RIPE.12.5 открыт. + +## 12.5.140 — специальные IPv4, RIPE и изолированная SQL-приёмка (2026-10-11) + +Исправлен isNonPublicIp: benchmark198.18/15, TEST-NET ranges, reserved240/4 и non-global IETF assignments исключены из внешнего RIPE lookup. Глобальные PCP/TURN anycast192.0.0.9/10 сохранены eligible. Фиксированные CIDR masks вычисляются один раз; hot flow predicate больше не разбирает строки CIDR/адреса для каждого range. IPFIX tuples, NAT, network config и DB schema не менялись.31 boundary/exception checks и stale public-cache bypass/no-fetch тесты passed; [IANA](https://www.iana.org/assignments/iana-ipv4-special-registry/). + +Local test:traffic-flow (20 scripts) exit0, но шесть SQL modules включали PG skips — не объявлены pass. Все шесть затем проверены against production candidate в четырёх собственных temporary PostgreSQL DBs. Parent создаёт exact own DB, child проверяет current_database до test import, DATABASE_PATH/GEOIP_DIR вынесены в ephemeral container; original DB/legacy SQLite не использовались fixtures. Все четыре DB удалены с owner/prefix check, включая failed runs. + +Initial failures сохранены и исправлены в tests: analytics зависел от чужого hardcoded server7 (FK); теперь создаётся own server, все SQL scoped, finally cleanup. Map-hops использовал TEST-NET как вымышленные публичные сервисы; fixtures получили eligible synthetic IPv4, assertions не ослаблены. Statistics не создавал historical server_snapshots partition; теперь создаёт exact fixed-date partition. Ingest/analytics/map-hops/purge/facts-rebuild/statistics SQL modules passed across bounded runs; прошедшие modules без необходимости не повторялись. Mock upstream failure fixture перенесён с TEST-NET на eligible address и явно подтверждает реальный вызов stub. + +Changed-file ESLint, backend production build, RIPE/IP profile tests passed. Реальный production RIPE service снова обогатил два публичных IP; повторный public cache hit и special/private controls дают0 дополнительных requests. Действует backend ux-ripe140/frontend ux-log133; backend/PG healthy. Все984 emitted outputs совпали с candidate Docker, actual deployed IP service SHA256 совпал; private rollback env/old image сохранены. [Полные попытки/проверки/границы](ux-ui/acceptance/flow-special-ipv4-2026-10-11.json). + +Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. При этом traffic отправил initial requests /api/traffic/servers/srv1 и /interfaces, оба400/id is required; это existing mock-id hydration/catalog gate defect, не часть IPv4 fix и не скрытый success. [Smoke](ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json). Следующий141 — gate live fetch до подтверждённого numeric server id/catalog. IPv6 special ranges — отдельно. Full UI1465 accepted139 до backend-only140; backend/SQL/live delta проверены отдельно, frontend не менялся.12.5 открыт. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index ddb9f5f2..37309d78 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Сервер161.104.46.222: завершён12.5.139 — общая UI1244/1244 и context221/221 (1465 unique, без retry),984 emitted backend outputs совпали с Docker ux-log133.125 generated evidence files restored, исходные hashes сохранены. RIPE real2IPs enrichment positive, reserved198.18 no-fetch control failed. Следующий140 — исправить специальные IPv4 ranges, профильные проверки/build/deploy/live RIPE.12.5 открыт. +Сервер161.104.46.222: завершён12.5.140 — special IPv4 RIPE guard fixed,31 boundary cases/stale-cache/no-fetch checks;20 local flow scripts exit0 с честными PG skips,6 unique SQL modules passed в own DBs/removed4. Live RIPE passed,compiled984 parity; backend ux-ripe140/frontend ux-log133. Smoke выявил initial traffic srv1→400, не скрыт. Следующий141 — gate live traffic до numeric/catalog-confirmed ID; IPv6 registry отдельно.12.5 открыт. | Параметр | Проверенное состояние | | --- | --- | @@ -14,7 +14,7 @@ | PostgreSQL | postgres:18-alpine,18.6;mmapp-test-postgres healthy;SELECT version() passed | | Данные | Отдельный Compose project mmapp-test,том mmapp-test_mmapp-pgdata и каталог ./data | | Доступ | PostgreSQL не публикуется;frontend3000/backend8000 привязаны к127.0.0.1 после запуска | -| Образы приложения | mmapp-test/backend:ux-log133 и mmapp-test/frontend:ux-log133; запущены | +| Образы приложения | mmapp-test/backend:ux-ripe140 и mmapp-test/frontend:ux-log133; запущены | Compose SHA256:3052bd6ab5d9865f97605cebfb5ff27a4410cd3357b81153a65a87fb7c92582f. OptionalBACKEND_IMAGE_TAG позволяетобновлятьbackendотдельно. Локальный и удалённый файлы совпали. Шаблоны: [Compose](../deploy/docker-compose.test.yml), [env без секретов](../deploy/env.test.example). @@ -1230,3 +1230,15 @@ Own issuer chain удалена только после доказательст Реальный production RIPE service в отдельном процессе, без чтения/записи рабочего кеша, обогатил194.226.121.150 и104.165.244.7: network-info/maxmind-geo-lite/as-overview200, prefix/ASN/holder/country/coordinates получены. Это provider metadata, не гарантия физического положения хоста и не complete NetFlow pipeline. Дополнительный no-fetch контроль с198.18.139.1 failed: benchmark IPv4 ошибочно отправлен upstream. Private192.168/loopback контроль не вызвал запросов; public cache hits проверяются отдельно в следующем блоке. Этот отказ сохранён, весь RIPE test не объявлен success. [Регрессия, compiled hashes и partial RIPE](ux-ui/acceptance/regression-runtime-ripe-2026-10-11.json). Перед регрессией сохранены586 existing evidence files.125 автоматически изменённых файлов восстановлены; все исходные SHA256 совпали, включая восемь pre-existing dirty GRE/VXLAN PNG. Новые135–138 evidence не затёрты. Подробные временные logs/snapshots в ignored tmp; побочные generated screenshots не включены в коммит. Source/runtime не менялись; backend/frontend ux-log133. Следующий140 — исправление фильтра специальных IPv4 согласно IANA, профильные backend/flow проверки, build/deploy и повторный live RIPE.12.5 открыт. + +## 12.5.140 — специальные IPv4, RIPE и изолированная SQL-приёмка (2026-10-11) + +Исправлен isNonPublicIp: benchmark198.18/15, TEST-NET ranges, reserved240/4 и non-global IETF assignments исключены из внешнего RIPE lookup. Глобальные PCP/TURN anycast192.0.0.9/10 сохранены eligible. Фиксированные CIDR masks вычисляются один раз; hot flow predicate больше не разбирает строки CIDR/адреса для каждого range. IPFIX tuples, NAT, network config и DB schema не менялись.31 boundary/exception checks и stale public-cache bypass/no-fetch тесты passed; [IANA](https://www.iana.org/assignments/iana-ipv4-special-registry/). + +Local test:traffic-flow (20 scripts) exit0, но шесть SQL modules включали PG skips — не объявлены pass. Все шесть затем проверены against production candidate в четырёх собственных temporary PostgreSQL DBs. Parent создаёт exact own DB, child проверяет current_database до test import, DATABASE_PATH/GEOIP_DIR вынесены в ephemeral container; original DB/legacy SQLite не использовались fixtures. Все четыре DB удалены с owner/prefix check, включая failed runs. + +Initial failures сохранены и исправлены в tests: analytics зависел от чужого hardcoded server7 (FK); теперь создаётся own server, все SQL scoped, finally cleanup. Map-hops использовал TEST-NET как вымышленные публичные сервисы; fixtures получили eligible synthetic IPv4, assertions не ослаблены. Statistics не создавал historical server_snapshots partition; теперь создаёт exact fixed-date partition. Ingest/analytics/map-hops/purge/facts-rebuild/statistics SQL modules passed across bounded runs; прошедшие modules без необходимости не повторялись. Mock upstream failure fixture перенесён с TEST-NET на eligible address и явно подтверждает реальный вызов stub. + +Changed-file ESLint, backend production build, RIPE/IP profile tests passed. Реальный production RIPE service снова обогатил два публичных IP; повторный public cache hit и special/private controls дают0 дополнительных requests. Действует backend ux-ripe140/frontend ux-log133; backend/PG healthy. Все984 emitted outputs совпали с candidate Docker, actual deployed IP service SHA256 совпал; private rollback env/old image сохранены. [Полные попытки/проверки/границы](ux-ui/acceptance/flow-special-ipv4-2026-10-11.json). + +Readonly live traffic/statistics smoke320: rendering200/no overflow/pageerror0. При этом traffic отправил initial requests /api/traffic/servers/srv1 и /interfaces, оба400/id is required; это existing mock-id hydration/catalog gate defect, не часть IPv4 fix и не скрытый success. [Smoke](ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json). Следующий141 — gate live fetch до подтверждённого numeric server id/catalog. IPv6 special ranges — отдельно. Full UI1465 accepted139 до backend-only140; backend/SQL/live delta проверены отдельно, frontend не менялся.12.5 открыт. diff --git a/docs/ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json b/docs/ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json new file mode 100644 index 00000000..6ddd8e95 --- /dev/null +++ b/docs/ux-ui/acceptance/browser-flow-special-ip-2026-10-11.json @@ -0,0 +1,55 @@ +{ + "date": "2026-10-11", + "runtime": { + "backend": "ux-ripe140", + "frontend": "ux-log133" + }, + "cases": [ + { + "path": "/traffic", + "width": 320, + "httpStatus": 200, + "noOverflow": true, + "apiResponses": [ + 200, + 200, + 400, + 400, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ] + }, + { + "path": "/statistics", + "width": 320, + "httpStatus": 200, + "noOverflow": true, + "apiResponses": [ + 200, + 200, + 200, + 200, + 200, + 200 + ] + } + ], + "pageErrors": [], + "bounds": [ + "Read-only smoke on empty/current lab catalogs; no full NetFlow pipeline or throughput claim" + ] +} diff --git a/docs/ux-ui/acceptance/flow-special-ipv4-2026-10-11.json b/docs/ux-ui/acceptance/flow-special-ipv4-2026-10-11.json new file mode 100644 index 00000000..c372c53c --- /dev/null +++ b/docs/ux-ui/acceptance/flow-special-ipv4-2026-10-11.json @@ -0,0 +1,216 @@ +{ + "date": "2026-10-11", + "block": "12.5.140", + "runtime": { + "backend": "ux-ripe140", + "frontend": "ux-log133", + "backendHealthy": true, + "postgresHealthy": true, + "frontendRunning": true + }, + "checks": { + "localTrafficFlowSeriesExit": 0, + "localPostgresSkipped": true, + "localRipeTestPassed": true, + "ipv4BoundaryTestsPassed": true, + "eslintChangedFilesPassed": true, + "backendProductionBuildPassed": true, + "isolatedSqlUniqueModulesPassed": [ + "traffic-flow-ingest", + "traffic-flow-analytics", + "traffic-flow-map-hops", + "traffic-flow-purge", + "traffic-flow-facts-rebuild", + "statistics-aggregate" + ], + "temporaryDatabasesRemoved": 4, + "compiledBackendOutputsMatched": 984, + "compiledMissing": 0, + "compiledDifferent": 0 + }, + "hashes": { + "ipServiceSource": "b164a6e50e61d235855c0888e3f178162fbb46affbef5cc534189401d7d14865", + "compiledIpService": "583b1ec8ee3f05d2a0a4e72ccd358cc3ecffd006aa4332dbdc72f63903dbbfd6", + "sourceAndRuntimeMatched": true + }, + "sqlAttempts": [ + { + "date": "2026-10-11", + "name": "mm_accept_140_1791697211345", + "tests": [ + { + "module": "traffic-flow-ingest", + "passed": true, + "output": "traffic-flow-ingest.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-analytics", + "passed": false, + "error": "Command failed: /usr/local/bin/node /app/backend/acceptance/pg140-runner.mjs /app/backend/acceptance/pg140-tests/traffic-flow-analytics.mjs\nSQLite → PostgreSQL: файл /tmp/mm_accept_140_1791697211345/empty.db не найден, база после wipe остаётся пустой (defaults settings)\ninsert or update on table \"flow_daily_dims_m_202609\" violates foreign key constraint \"flow_daily_dims_server_id_fkey\"\n", + "output": "", + "stderr": "SQLite → PostgreSQL: файл /tmp/mm_accept_140_1791697211345/empty.db не найден, база после wipe остаётся пустой (defaults settings)\ninsert or update on table \"flow_daily_dims_m_202609\" violates foreign key constraint \"flow_daily_dims_server_id_fkey\"\n" + } + ], + "databaseRemoved": true + }, + { + "date": "2026-10-11", + "name": "mm_accept_140_1791697661858", + "tests": [ + { + "module": "traffic-flow-ingest", + "passed": true, + "output": "traffic-flow-ingest.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-analytics", + "passed": true, + "output": "traffic-flow-analytics.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-map-hops", + "passed": false, + "error": "Command failed: /usr/local/bin/node /app/backend/acceptance/pg140-runner.mjs /app/backend/acceptance/pg140-tests/traffic-flow-map-hops.mjs\nSQLite → PostgreSQL: файл /tmp/mm_accept_140_1791697661858/empty.db не найден, база после wipe остаётся пустой (defaults settings)\nтоп-8 брендов на карте\n\n2 !== 8\n\n", + "output": "traffic-flow-map-hops.test.ts: pickMapServices ok\ntraffic-flow-map-hops.test.ts: hops ok\n", + "stderr": "SQLite → PostgreSQL: файл /tmp/mm_accept_140_1791697661858/empty.db не найден, база после wipe остаётся пустой (defaults settings)\nтоп-8 брендов на карте\n\n2 !== 8\n\n" + } + ], + "databaseRemoved": true + }, + { + "date": "2026-10-11", + "name": "mm_accept_140_1791697879954", + "tests": [ + { + "module": "traffic-flow-map-hops", + "passed": true, + "output": "traffic-flow-map-hops.test.ts: pickMapServices ok\ntraffic-flow-map-hops.test.ts: hops ok\ntraffic-flow-map-hops.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-purge", + "passed": true, + "output": "traffic-flow-purge.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "traffic-flow-facts-rebuild", + "passed": true, + "output": "traffic-flow-facts-rebuild.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + }, + { + "module": "statistics-aggregate", + "passed": false, + "error": "Command failed: /usr/local/bin/node /app/backend/acceptance/pg140-runner.mjs /app/backend/acceptance/pg140-tests/statistics-aggregate.mjs\nSQLite → PostgreSQL: файл /tmp/mm_accept_140_1791697879954/empty.db не найден, база после wipe остаётся пустой (defaults settings)\nno partition of relation \"server_snapshots\" found for row\n", + "output": "", + "stderr": "SQLite → PostgreSQL: файл /tmp/mm_accept_140_1791697879954/empty.db не найден, база после wipe остаётся пустой (defaults settings)\nno partition of relation \"server_snapshots\" found for row\n" + } + ], + "databaseRemoved": true + }, + { + "date": "2026-10-11", + "name": "mm_accept_140_1791698055809", + "tests": [ + { + "module": "statistics-aggregate", + "passed": true, + "output": "statistics-aggregate.test.ts: ok\nISOLATED_SQL_TEST_COMPLETE\n" + } + ], + "databaseRemoved": true + } + ], + "liveRipe": { + "date": "2026-10-11", + "runtime": "ux-ripe140", + "requests": [ + { + "path": "/data/network-info/data.json", + "resource": "104.165.244.7", + "status": 200 + }, + { + "path": "/data/network-info/data.json", + "resource": "194.226.121.150", + "status": 200 + }, + { + "path": "/data/maxmind-geo-lite/data.json", + "resource": "194.226.121.0/24", + "status": 200 + }, + { + "path": "/data/maxmind-geo-lite/data.json", + "resource": "104.165.244.0/24", + "status": 200 + }, + { + "path": "/data/as-overview/data.json", + "resource": "AS41745", + "status": 200 + }, + { + "path": "/data/as-overview/data.json", + "resource": "AS41745", + "status": 200 + } + ], + "cases": [ + { + "ip": "194.226.121.150", + "prefix": "194.226.121.0/24", + "asn": 41745, + "country": "RU", + "lat": 55.7487, + "lng": 37.6187, + "holder": "FORTIS-AS Baykov Ilya Sergeevich", + "ok": true, + "fetchedAt": 1791697689859 + }, + { + "ip": "104.165.244.7", + "prefix": "104.165.244.0/24", + "asn": 41745, + "country": "PL", + "lat": 52.2299, + "lng": 21.0093, + "holder": "FORTIS-AS Baykov Ilya Sergeevich", + "ok": true, + "fetchedAt": 1791697689949 + } + ], + "cacheHitsNoFetch": true, + "success": true + }, + "browserSmoke": { + "protocol": "browser-flow-special-ip-2026-10-11.json", + "pageRenderingAndNoOverflowPassed": true, + "apiFailures": [ + { + "path": "/api/traffic/servers/srv1", + "status": 400, + "error": "id is required" + }, + { + "path": "/api/traffic/servers/srv1/interfaces", + "status": 400, + "error": "id is required" + } + ], + "nextFix": "Gate live traffic fetches until confirmed numeric server id/catalog" + }, + "bounds": [ + "IPv4 eligible-public predicate updated using IANA non-global special ranges; PCP/TURN192.0.0.9/10 remain eligible; global reachability not guaranteed by eligibility", + "Fixed ranges parsed once for hot flow paths; no IPFIX tuple/NAT/network config/DB schema change", + "Stale seeded public metadata for special IPv4 bypassed; special IPv4/private/cache-hit enqueue produces no RIPE requests", + "Initial SQL failures retained: hardcoded server7/missing FK fixture; documentation IPs used as public service fixtures; missing historical snapshot partition", + "Test fixtures fixed without weakening data/assertions: owned server+scoped cleanup, eligible synthetic public IPs, partition for fixed test date", + "Six unique DB modules accepted across bounded runs, not six fresh successes in one run; local skips not counted as passes", + "Own temporary databases created/dropped with owner/prefix checks; child current_database checked before tests; working DB/legacy SQLite untouched", + "All984 production outputs match candidate and actual deployed IP service hash; no frontend rebuild because frontend source unchanged", + "Read-only UI smoke has no page errors/overflow but two existing initial mock-id400 requests; not a fully green API loading acceptance", + "IPv6 special registry handling and the traffic initial live-id gate remain separate work" + ], + "references": [ + "https://www.iana.org/assignments/iana-ipv4-special-registry/" + ] +}