From 48cc65ec2b509d9ca7c4e10538ad0cae80995e14 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 14 Oct 2026 15:04:20 +0700 Subject: [PATCH] =?UTF-8?q?fix(geoip):=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D1=82=D1=8C=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BA=D0=B5=D1=88=20=D0=B8=20=D0=BE=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B8=D1=82=D1=8C=20=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D1=83=20=D0=B1=D0=B0=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- backend/scripts/acceptance/geoip-mirror.mjs | 66 +++++++++ .../scripts/acceptance/run-geoip-mirror.sh | 32 +++++ backend/src/services/geoip-download.test.ts | 27 ++++ backend/src/services/geoip-download.ts | 38 ++++++ .../src/services/geoip-update-collector.ts | 35 +++-- .../src/services/traffic-flow-geoip.test.ts | 17 ++- docs/ux-ui-external-acceptance.md | 20 ++- docs/ux-ui-plan.md | 20 ++- docs/ux-ui-progress.md | 20 ++- docs/ux-ui-remaining-acceptance.md | 28 +++- docs/ux-ui-stage-12.md | 20 ++- docs/ux-ui-test-server.md | 20 ++- docs/ux-ui/acceptance/geoip-mirror242.json | 129 ++++++++++++++++++ .../ux-ui/acceptance/geoip242-deployment.json | 24 ++++ docs/ux-ui/acceptance/geoip242-live-ui.json | 25 ++++ 16 files changed, 492 insertions(+), 31 deletions(-) create mode 100644 backend/scripts/acceptance/geoip-mirror.mjs create mode 100644 backend/scripts/acceptance/run-geoip-mirror.sh create mode 100644 backend/src/services/geoip-download.test.ts create mode 100644 backend/src/services/geoip-download.ts create mode 100644 docs/ux-ui/acceptance/geoip-mirror242.json create mode 100644 docs/ux-ui/acceptance/geoip242-deployment.json create mode 100644 docs/ux-ui/acceptance/geoip242-live-ui.json diff --git a/backend/package.json b/backend/package.json index d3ede88d..cd5be3fa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -34,7 +34,7 @@ "test:evolution": "tsx src/services/evobgp-catalog.test.ts && tsx src/routes/evobgp-catalog.test.ts && tsx src/routes/evobgp-settings.test.ts && tsx src/plugins/auth.evolution-settings.test.ts", "test:netflow-purge": "tsx src/db/operation-lock.test.ts && tsx src/services/traffic-flow-purge-execution.test.ts && tsx src/routes/traffic-flow-purge.test.ts", "test:netflow-settings": "tsx src/services/traffic-flow-host-file-builders.test.ts && tsx src/services/traffic-flow-key-execution.test.ts && tsx src/routes/traffic-flow-settings.test.ts && tsx src/plugins/auth.netflow-settings.test.ts", - "test:geoip": "tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts", + "test:geoip": "tsx src/services/geoip-download.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/plugins/auth.geoip.test.ts", "test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/services/ospf-effective-disabled.test.ts && tsx src/routes/ospf-optimizer.test.ts", "test:containers": "tsx src/services/containers-state.test.ts && tsx src/routes/containers-state.test.ts", "test:certificates": "tsx src/services/certificate-issue-execution.test.ts && tsx src/services/certificate-issue-request.test.ts && tsx src/routes/certificates-issue.test.ts && tsx src/services/certificate-issue-dto.test.ts && tsx src/services/certificate-parse.test.ts && tsx src/services/certificates-state.test.ts && tsx src/routes/certificates-state.test.ts", diff --git a/backend/scripts/acceptance/geoip-mirror.mjs b/backend/scripts/acceptance/geoip-mirror.mjs new file mode 100644 index 00000000..306c9d84 --- /dev/null +++ b/backend/scripts/acceptance/geoip-mirror.mjs @@ -0,0 +1,66 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import { createHash } from 'node:crypto'; +const database = process.env.MMAPP_OWN_SQL_DB; +assert(/^mm_accept_166_\d+$/.test(database)); +const url = new URL(process.env.DATABASE_URL); assert.equal(url.pathname, '/' + database); assert.equal(url.username, database); +assert.equal(process.env.GEOIP_DIR, '/tmp/' + database + '/geoip'); +fs.mkdirSync('/tmp/' + database, { recursive: true }); process.chdir('/tmp/' + database); +const { pool, closePool } = await import('/app/backend/dist/db/index.js'); +const { initDatabase } = await import('/app/backend/dist/db/bootstrap.js'); +const role = (await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]; +assert(Object.values(role).every(v => v === false)); await initDatabase(); +const { collectGeoipUpdateOnce, setGeoipFetchForTests, resetGeoipUpdateForTests } = await import('/app/backend/dist/services/geoip-update-collector.js'); +const { getGeoipSettings, updateGeoipSettings } = await import('/app/backend/dist/services/geoip-settings.js'); +const { geoipReadersStatus, lookupGeoip, GEOIP_COUNTRY_FILE, GEOIP_ASN_FILE } = await import('/app/backend/dist/services/traffic-flow-geoip.js'); +const { databaseBackgroundWork } = await import('/app/backend/dist/services/database-background-work.js'); +const report = { date: '2026-10-14', block: '12.5.242', passed: false, scope: 'Compiled GeoIP collector + owned PostgreSQL/files + actual public mirror; controlled negative bodies separately marked', mirrorRequests: [], checks: [], chrContacts: 0, mainDatabaseWrites: 0 }; +const file = name => path.join(process.env.GEOIP_DIR, name); +const sha = name => createHash('sha256').update(fs.readFileSync(file(name))).digest('hex'); +const save = () => fs.writeFileSync('/checks/geoip-mirror242.json', JSON.stringify(report, null, 2) + '\n'); +const check = (name, detail = {}) => { report.checks.push({ name, ...detail }); save(); }; +const realFetch = globalThis.fetch.bind(globalThis); +async function observedFetch(url, options) { + const started = Date.now(), response = await realFetch(url, options); + report.mirrorRequests.push({ file: new URL(String(url)).pathname.split('/').at(-1), conditional: Boolean(options?.headers?.['If-None-Match']), status: response.status, headersMs: Date.now() - started }); save(); return response; +} +try { + await updateGeoipSettings({ enabled: false }); setGeoipFetchForTests(observedFetch); + let snapshot = await collectGeoipUpdateOnce({ force: true }); + assert(!snapshot.fatalError); assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 2); + assert(geoipReadersStatus().countryLoaded && geoipReadersStatus().asnLoaded); + const initial = { country: sha(GEOIP_COUNTRY_FILE), asn: sha(GEOIP_ASN_FILE) }; + const lookup = lookupGeoip('8.8.8.8'); assert.equal(lookup?.country, 'US'); assert.equal(lookup?.asn, 15169); + check('actual-two-downloads-valid-mmdb-and-lookup', { bytes: snapshot.bytes, hashes: initial, settings: await getGeoipSettings(), lookup: { country: lookup.country, asn: lookup.asn } }); + const first = collectGeoipUpdateOnce({ force: true }); + await assert.rejects(collectGeoipUpdateOnce({ force: true }), e => e.statusCode === 409); + assert.equal((await collectGeoipUpdateOnce()).skipped, true); + snapshot = await first; assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.skippedUnchanged, 2); assert.equal(snapshot.downloaded, 0); + check('actual-conditional304-and-concurrent409', { skippedUnchanged: 2, hashesUnchanged: sha(GEOIP_COUNTRY_FILE) === initial.country && sha(GEOIP_ASN_FILE) === initial.asn }); + fs.unlinkSync(file(GEOIP_COUNTRY_FILE)); + const beforeMissing = report.mirrorRequests.length; snapshot = await collectGeoipUpdateOnce({ force: true }); + assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 1); assert.equal(snapshot.skippedUnchanged, 1); + assert(report.mirrorRequests.slice(beforeMissing).some(r => r.file === GEOIP_COUNTRY_FILE && !r.conditional && r.status === 200)); + assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country); + check('missing-country-refetched-without-conditional-header'); + fs.writeFileSync(file(GEOIP_COUNTRY_FILE), 'Own corrupt GeoIP fixture'); + snapshot = await collectGeoipUpdateOnce({ force: true }); assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 1); assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country); + check('corrupt-local-country-refetched-and-validated'); + const lastSuccess = (await getGeoipSettings()).lastSuccessAt; + setGeoipFetchForTests(async (url, options) => String(url).endsWith(GEOIP_COUNTRY_FILE) ? new Response('Own invalid MMDB body', { status: 200 }) : observedFetch(url, options)); + snapshot = await collectGeoipUpdateOnce({ force: true }); assert.equal(snapshot.errors.length, 1); assert.equal(snapshot.downloaded, 0); assert.equal(snapshot.skippedUnchanged, 1); + assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country); assert.equal((await getGeoipSettings()).lastSuccessAt, lastSuccess); + assert((await getGeoipSettings()).lastError); assert(!fs.existsSync(file(GEOIP_COUNTRY_FILE) + '.tmp')); + check('controlled-invalid-body-preserves-file-and-last-success', { negativeCountryResponse: 'controlled-200-invalid-MMDB', realAsnStatus: 304 }); + fs.unlinkSync(file(GEOIP_COUNTRY_FILE)); + setGeoipFetchForTests(async (url, options) => String(url).endsWith(GEOIP_COUNTRY_FILE) ? new Response(null, { status: 304 }) : observedFetch(url, options)); + snapshot = await collectGeoipUpdateOnce({ force: true }); assert.equal(snapshot.errors.length, 1); assert.equal(snapshot.skippedUnchanged, 1); assert(snapshot.errors[0].includes('304 без проверенной локальной базы')); + assert.equal((await getGeoipSettings()).lastSuccessAt, lastSuccess); + check('controlled-uncached304-is-error-with-last-success-retained'); + setGeoipFetchForTests(observedFetch); snapshot = await collectGeoipUpdateOnce({ force: true }); assert.deepEqual(snapshot.errors, []); assert.equal(snapshot.downloaded, 1); + assert.equal(sha(GEOIP_COUNTRY_FILE), initial.country); assert.equal(sha(GEOIP_ASN_FILE), initial.asn); + const beforeSkip = report.mirrorRequests.length; assert.equal((await collectGeoipUpdateOnce()).skipped, true); assert.equal(report.mirrorRequests.length, beforeSkip); + check('actual-recovery-and-disabled-background-no-fetch'); report.passed = true; +} catch (e) { report.failure = { name: e.name }; process.exitCode = 1; } +finally { resetGeoipUpdateForTests(); await databaseBackgroundWork.drain(); await closePool(); save(); console.log(JSON.stringify(report)); } diff --git a/backend/scripts/acceptance/run-geoip-mirror.sh b/backend/scripts/acceptance/run-geoip-mirror.sh new file mode 100644 index 00000000..0edc03d4 --- /dev/null +++ b/backend/scripts/acceptance/run-geoip-mirror.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail +umask 077 +cd /opt/mmapp-test +mm242_db="mm_accept_166_$(date +%s%3N)" +mm242_pass="$(openssl rand -hex 24)" +mm242_role_created=false +mm242_db_created=false +cleanup242() { + docker rm mmapp-s3app242-owned >/dev/null 2>&1 || true + if $mm242_db_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP DATABASE \"$mm242_db\" WITH (FORCE)"; fi + if $mm242_role_created; then docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "DROP ROLE \"$mm242_db\""; fi + remaining=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm242_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm242_db')") + [ "$remaining" = 0 ] + python3 - <<'PY' +import json,pathlib +p=pathlib.Path('/opt/mmapp-test/artifacts/backup242-checks/geoip-mirror242.json') +if p.exists(): + r=json.loads(p.read_text());r['ownedDatabaseAndRoleRemoved']=True;p.write_text(json.dumps(r,indent=2)+'\n') +PY + printf 'OWNED_DATABASE_AND_ROLE_REMOVED\n' + unset mm242_pass +} +trap cleanup242 EXIT +[[ "$mm242_db" =~ ^mm_accept_166_[0-9]+$ ]] +existing=$(docker exec mmapp-test-postgres psql -U mmapp -d mmapp -Atc "SELECT (SELECT count(*) FROM pg_database WHERE datname='$mm242_db')+(SELECT count(*) FROM pg_roles WHERE rolname='$mm242_db')") +[ "$existing" = 0 ] +printf "CREATE ROLE \"%s\" LOGIN PASSWORD '%s' NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;\n" "$mm242_db" "$mm242_pass" | docker exec -i mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 +mm242_role_created=true +docker exec mmapp-test-postgres psql -U mmapp -d mmapp -v ON_ERROR_STOP=1 -c "CREATE DATABASE \"$mm242_db\" OWNER \"$mm242_db\"" +mm242_db_created=true +docker run --name mmapp-s3app242-owned --network mmapp-test_default --cap-drop ALL --security-opt no-new-privileges -e "MMAPP_ORIGIN_BASELINE=${2:-false}" -e NODE_ENV=test -e "DATABASE_URL=postgres://$mm242_db:$mm242_pass@postgres:5432/$mm242_db" -e "MMAPP_OWN_SQL_DB=$mm242_db" -e "DATABASE_PATH=/tmp/$mm242_db/legacy.db" -e "GEOIP_DIR=/tmp/$mm242_db/geoip" -v /opt/mmapp-test/artifacts/backup242-checks:/checks --entrypoint node "${1:-mmapp-test/backend:ux-geoip242}" /checks/geoip-mirror.mjs diff --git a/backend/src/services/geoip-download.test.ts b/backend/src/services/geoip-download.test.ts new file mode 100644 index 00000000..476cfc50 --- /dev/null +++ b/backend/src/services/geoip-download.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict" +import { createServer } from "node:http" +import { downloadGeoipDatabase } from "./geoip-download.js" + +let stalledClosed = false +const server = createServer((req, res) => { + if (req.url === "/slow") { res.writeHead(200); res.write("partial"); res.on("close", () => { stalledClosed = true }) } + else if (req.url === "/chunked") { res.writeHead(200); res.write("1234"); res.end("5678") } + else if (req.url === "/oversized") { res.writeHead(200, { "content-length": "9999" }); res.end("oversized") } + else if (req.url === "/304") { res.writeHead(304); res.end() } + else { res.writeHead(200); res.end("valid fixture") } +}) +await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)) +const address = server.address(); assert(address && typeof address !== "string") +const base = `http://127.0.0.1:${address.port}` +try { + const valid = await downloadGeoipDatabase(base, {}, fetch, 2000, 100) + assert.equal(valid.body.toString(), "valid fixture") + assert.equal((await downloadGeoipDatabase(base + "/304", {}, fetch, 2000, 100)).response.status, 304) + await assert.rejects(downloadGeoipDatabase(base + "/slow", {}, fetch, 120, 100), error => (error as Error).name === "AbortError") + await assert.rejects(downloadGeoipDatabase(base + "/oversized", {}, fetch, 2000, 5), /превышает/) + await assert.rejects(downloadGeoipDatabase(base + "/chunked", {}, fetch, 2000, 5), /превышает/) + // A failed request leaves no persistent timer or lock preventing the next successful download. + assert.equal((await downloadGeoipDatabase(base, {}, fetch, 2000, 100)).body.toString(), "valid fixture") + await new Promise(resolve => setTimeout(resolve, 20)); assert(stalledClosed) + console.log("GeoIP HTTP: whole-body timeout, declared/chunked limits,304 and recovery passed") +} finally { server.closeAllConnections(); await new Promise(resolve => server.close(() => resolve())) } diff --git a/backend/src/services/geoip-download.ts b/backend/src/services/geoip-download.ts new file mode 100644 index 00000000..d8877230 --- /dev/null +++ b/backend/src/services/geoip-download.ts @@ -0,0 +1,38 @@ +const DOWNLOAD_TIMEOUT_MS = 120_000 +const MAX_DOWNLOAD_BYTES = 64 * 1024 * 1024 + +/** A single deadline covers headers and streaming; a mirror cannot allocate an unbounded body. */ +export async function downloadGeoipDatabase( + url: string, headers: Record, fetcher: typeof fetch, + timeoutMs = DOWNLOAD_TIMEOUT_MS, maxBytes = MAX_DOWNLOAD_BYTES, +): Promise<{ response: Response; body: Buffer }> { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + try { + const response = await fetcher(url, { headers, signal: controller.signal }) + if (!response.ok || !response.body) { + await response.body?.cancel().catch(() => {}) + return { response, body: Buffer.alloc(0) } + } + const size = Number(response.headers.get("content-length")) + if (Number.isFinite(size) && size > maxBytes) { + await response.body.cancel().catch(() => {}) + throw new Error("Размер базы GeoIP превышает допустимый предел") + } + const reader = response.body.getReader(), chunks: Buffer[] = [] + let received = 0 + try { + for (;;) { + const { value, done } = await reader.read() + if (done) break + received += value.byteLength + if (received > maxBytes) { + await reader.cancel().catch(() => {}) + throw new Error("Размер базы GeoIP превышает допустимый предел") + } + chunks.push(Buffer.from(value)) + } + return { response, body: Buffer.concat(chunks, received) } + } finally { reader.releaseLock() } + } finally { clearTimeout(timer) } +} diff --git a/backend/src/services/geoip-update-collector.ts b/backend/src/services/geoip-update-collector.ts index aae66eb8..43462ed9 100644 --- a/backend/src/services/geoip-update-collector.ts +++ b/backend/src/services/geoip-update-collector.ts @@ -4,6 +4,7 @@ import { open, type AsnResponse, type CountryResponse } from "maxmind" import type { GeoipUpdateRunSnapshot } from "../types/scheduler-run-snapshot.js" import { SCHEDULER_RUN_SNAPSHOT_VERSION } from "../types/scheduler-run-snapshot.js" import { getGeoipEtags, getGeoipSettings, touchGeoipRunMeta } from "./geoip-settings.js" +import { downloadGeoipDatabase } from "./geoip-download.js" import { GEOIP_ASN_FILE, GEOIP_COUNTRY_FILE, @@ -13,7 +14,6 @@ import { /** Зеркало GeoLite2 без регистрации и ключей (см. README: GeoIP). */ const MIRROR_BASE = "https://github.com/P3TERX/GeoLite.mmdb/raw/download" -const DOWNLOAD_TIMEOUT_MS = 120_000 /** Пробный IP для валидации скачанной базы: Google DNS. */ const PROBE_IP = "8.8.8.8" @@ -83,11 +83,7 @@ export async function collectGeoipUpdateOnce( return emptySnapshot(sampledAt, true) } - const settings = await getGeoipSettings() - if (!settings.enabled && !opts.force) { - return emptySnapshot(sampledAt, true) - } - + // Acquire before the first await: concurrent requests must not both pass the gate. updating = true const snapshot: GeoipUpdateRunSnapshot = { v: SCHEDULER_RUN_SNAPSHOT_VERSION, @@ -101,6 +97,8 @@ export async function collectGeoipUpdateOnce( } try { + const settings = await getGeoipSettings() + if (!settings.enabled && !opts.force) return emptySnapshot(sampledAt, true) const dir = geoipDir() await mkdir(dir, { recursive: true }) const storedEtags = await getGeoipEtags() @@ -114,25 +112,26 @@ export async function collectGeoipUpdateOnce( const tmp = `${target}.tmp` const prevEtag = storedEtags[file] try { - const ac = new AbortController() - const timer = setTimeout(() => ac.abort(), DOWNLOAD_TIMEOUT_MS) - let res: Response - try { - res = await fetchImpl(`${MIRROR_BASE}/${file}`, { - headers: prevEtag ? { "If-None-Match": prevEtag } : {}, - signal: ac.signal, - }) - } finally { - clearTimeout(timer) + let cachedFileValid = false + if (prevEtag) { + try { + await (kind === "country" ? validateCountry(target) : validateAsn(target)) + cachedFileValid = true + } catch { /* A missing or invalid local cache requires a full download. */ } } + const { response: res, body } = await downloadGeoipDatabase( + `${MIRROR_BASE}/${file}`, + prevEtag && cachedFileValid ? { "If-None-Match": prevEtag } : {}, + fetchImpl, + ) if (res.status === 304) { + if (!cachedFileValid || !prevEtag) throw new Error("Зеркало вернуло HTTP 304 без проверенной локальной базы") snapshot.skippedUnchanged += 1 if (prevEtag) etags[file] = prevEtag continue } if (!res.ok) throw new Error(`HTTP ${res.status}`) const etag = res.headers.get("etag") ?? "" - const body = Buffer.from(await res.arrayBuffer()) snapshot.bytes += body.byteLength await writeFile(tmp, body) buildAt[kind] = @@ -161,7 +160,7 @@ export async function collectGeoipUpdateOnce( await touchGeoipRunMeta({ lastCheckAt: sampledAt, - lastSuccessAt: snapshot.errors.length ? null : sampledAt, + ...(snapshot.errors.length ? {} : { lastSuccessAt: sampledAt }), lastError: snapshot.errors.length ? snapshot.errors.join("; ") : null, countryBuildAt: buildAt.country, asnBuildAt: buildAt.asn, diff --git a/backend/src/services/traffic-flow-geoip.test.ts b/backend/src/services/traffic-flow-geoip.test.ts index e84445d7..4ea28188 100644 --- a/backend/src/services/traffic-flow-geoip.test.ts +++ b/backend/src/services/traffic-flow-geoip.test.ts @@ -141,7 +141,7 @@ const dims = minuteDimsSnapshotForTests() assert.equal(dims.get("country")?.get("US")?.bytes, 1000) assert.equal(dims.get("asn")?.get("15169")?.bytes, 1000) -// ── коллектор: 304 → обе базы без изменений ─────────────────────────────────── +// ── коллектор: 304 без локального cache не является успехом ────────────────── disableGeoipDbForTests() resetGeoipUpdateForTests() const geoipDir = mkdtempSync(path.join(tmpdir(), "mm-geoip-test-")) @@ -149,7 +149,9 @@ process.env.GEOIP_DIR = geoipDir setGeoipFetchForTests(async () => new Response(null, { status: 304 })) let snap = await collectGeoipUpdateOnce({ force: true }) -assert.equal(snap.skippedUnchanged, 2) +assert.equal(snap.skippedUnchanged, 0) +assert.equal(snap.errors.length, 2) +assert(snap.errors.every(message => message.includes("304 без проверенной локальной базы"))) assert.equal(snap.downloaded, 0) assert.equal(existsSync(path.join(geoipDir, "GeoLite2-Country.mmdb")), false) @@ -195,6 +197,17 @@ assert.equal(readFileSync(asnPath, "utf8"), "good-asn", "asn не тронут") assert.equal(existsSync(`${countryPath}.tmp`), false) assert.equal(existsSync(`${asnPath}.tmp`), false) +// Lock is acquired before settings lookup yields; a concurrent forced run rejects. +let releaseFetch!: () => void +const heldFetch = new Promise(resolve => { releaseFetch = resolve }) +setGeoipFetchForTests(async () => { await heldFetch; return new Response(null, { status: 503 }) }) +const firstRun = collectGeoipUpdateOnce({ force: true }) +await assert.rejects(collectGeoipUpdateOnce({ force: true }), error => (error as { statusCode?: number }).statusCode === 409) +assert.equal((await collectGeoipUpdateOnce()).skipped, true) +releaseFetch() +await firstRun +assert.equal((await collectGeoipUpdateOnce({ force: true })).errors.length, 2, "lock released after errors") + rmSync(geoipDir, { recursive: true, force: true }) delete process.env.GEOIP_DIR resetGeoipUpdateForTests() diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index bb9636a8..c65c8a44 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-14:241 — после237 приняты1000 смешанных local/missing,both/missing,unknown,available строк:ПК1440/320, rendered50, DOM2477/2239, search/last-good/pagination/keyboard passed, scoped axe0, writes0. Runtime backend/frontend ux-backups237. S3 native/limitedUI/retention230–240 приняты в записанных границах. Следующий242 — доступный mirror GeoIP job в изолированном scope; paid MaxMind/Cloudflare/Gitea/container jobs отдельно. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:242 — GeoIP mirror job принят в own SQL/files и реальном mirror:12 fetches200/304, две валидные MMDB12,380,575bytes, missing/corrupt recovery, concurrent409, partial error сохраняетlastSuccessAt. Исправлены false304/eager gate/body deadline; download120s/64MiB, HTTP boundary tests и15E2E прошли. Backend ux-geoip242 healthy, frontend ux-backups237 image/container unchanged; live readUI1440/320 passed. Следующий243 — аудит доступных container/certificate/release jobs и актуальных внешних ограничений. Этап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). @@ -18,6 +18,24 @@ OSPF84: real GRE Full adjacency/private routes/Ping3/3 и4 live PC/320px URL/bro BFD85: Up/Full/own route/Ping,4 live UI и loss/withdrawal/recovery приняты; exact own cleanup8 коллекций×2 passed. Withdrawal5,245ms не подтверждает sub-second BFD timing; этот вопрос открыт. Runtime routing83 без новой сборки. [Отчёт](ux-ui/acceptance/ospf-bfd-loss-2026-10-09.json). +## 12.5.242 — реальный GeoIP mirror job и честное состояние cache (2026-10-14) + +В collector обнаруженыfalse-success304 при отсутствующей локальной базе (старый тест прямо принимал это), race до acquisition lock из-за await settings и освобождение timeout после headers вместо body. Теперь gate захватывается до первогоawait; concurrent force409/backgroundskip. Conditional ETag отправляется только после проверки локальной MMDB; missing/corrupt cache требует200 полного файла.304 без validated cache — ошибка, неskippedUnchanged. Partial failures сохраняют дату последнего успешного update и предыдущий файл, добавляя lastError. Новая downloadGeoipDatabase держит120s deadline через streaming body, ограничивает declared/chunked file64MiB и закрывает непригодное тело. Базы проходят existing Country/ASN validation до atomic swap; формат/layout ReUI не менялись. + +Local test:geoip прошёл: реальный HTTP healthy/304, stalls after headers→AbortError, declared/chunked oversize→reject, recovery; collector invalid/cache/concurrent gates; JWT reader cannotmutate/writer/admin allowed. Backend/contracts build и scoped ESLint прошли.15 unique GeoIP settings/responsive/data-collection E2E прошли25.2s; ошибочная первоначальная команда с несуществующимиspec далаNo tests found и не считалась проверкой. Три автоматически перегенерированных старыхGeoIP PNG возвращены к исходным байтам; чужие GRE/VXLAN changes сохранены. + +Final production source516/compiled1072 matched. В own PostgreSQL/files scope выполнены7 укрупнённых сценариев: actual mirror download обеих MMDB12,380,575bytes, Google8.8.8.8→US/ASN15169, actual conditional304/hash unchanged иconcurrent409, missing-country refetch безETag, corrupt-country refetch, controlled invalid MMDB200/uncached304 сохраняютlastSuccessAt, actual recovery иdisabled background0fetch.12 реальных mirror GET200/304; negative country responses явно контролируемые и не выдаются за native WAN failure. Валидация MMDB не подменялась. Temporary DB/role/container/files удалены, CHRcontacts0/mainDBwrites0 в этом scope. Предварительная сборка с иным downloader отдельно проверялась; повтор не прибавлен кunique приёмке. + +Развёрнут только backend ux-geoip242; frontend ux-backups237 image/container ID неизменны. Backendhealthy; real Portal/live GET GeoIP200 иUI1440/320, обе main bases alreadyloaded, globaloverflow0/pageerrors0, POSTupdate0. Main GeoIP job через UI не запускался; реальная загрузка доказана в isolated scope, а не заявлена обновлением данных приложения. Откатbackend ux-backups237 и приватная прежняя.env сохранены на сервере. + +Протоколы: [mirror/SQL](ux-ui/acceptance/geoip-mirror242.json), [deployment](ux-ui/acceptance/geoip242-deployment.json), [live read UI](ux-ui/acceptance/geoip242-live-ui.json). MirrorP3TERX — фактический источник collector без account key. Прямой licensed MaxMind download не реализован текущим collector и не принят этим тестом; актуальность/геолокационная точность всех IP, publicIPv6 geography, multi-process lock, physical WAN failures отдельно. + +| Референс | Схема состояний | +| --- | --- | +| [ReUI Settings16](https://reui.io/preview/base/settings-16?ref=mcp) | validated cache → conditional304; missing/corrupt → full200 → validate → swap; error → last-good + last-success + warning | + +Следующий243 — read-only availability audit контейнерных/ACME/release jobs на текущем стенде и согласованная матрица реальных remaining dependencies. Не выдумывать Cloudflare/registry credentials и не обновлять RouterOS без отдельной команды. + ## 12.5.241 — тысяча бэкапов со смешанными состояниями файлов (2026-10-14) Изменение237 проверено на1000 обычных live-path HTTP DTO (199062bytes), по250 local/missing,both/missing,local/unknown,local/available. Два сценария1440/320 прошли25.2s. UI сохраняет pagination50 строк:DOM2477/2239, ready1203/806ms в dev fixture окружении. Это измерение конкретного запуска, не production SLA/сетевой latency и не доказательство скорости filesystem API1000 файлов. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 561b04a4..bab7b941 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,9 +1,27 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-14:241 — после237 приняты1000 смешанных local/missing,both/missing,unknown,available строк:ПК1440/320, rendered50, DOM2477/2239, search/last-good/pagination/keyboard passed, scoped axe0, writes0. Runtime backend/frontend ux-backups237. S3 native/limitedUI/retention230–240 приняты в записанных границах. Следующий242 — доступный mirror GeoIP job в изолированном scope; paid MaxMind/Cloudflare/Gitea/container jobs отдельно. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:242 — GeoIP mirror job принят в own SQL/files и реальном mirror:12 fetches200/304, две валидные MMDB12,380,575bytes, missing/corrupt recovery, concurrent409, partial error сохраняетlastSuccessAt. Исправлены false304/eager gate/body deadline; download120s/64MiB, HTTP boundary tests и15E2E прошли. Backend ux-geoip242 healthy, frontend ux-backups237 image/container unchanged; live readUI1440/320 passed. Следующий243 — аудит доступных container/certificate/release jobs и актуальных внешних ограничений. Этап12.5 открыт. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-14. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. +## 12.5.242 — реальный GeoIP mirror job и честное состояние cache (2026-10-14) + +В collector обнаруженыfalse-success304 при отсутствующей локальной базе (старый тест прямо принимал это), race до acquisition lock из-за await settings и освобождение timeout после headers вместо body. Теперь gate захватывается до первогоawait; concurrent force409/backgroundskip. Conditional ETag отправляется только после проверки локальной MMDB; missing/corrupt cache требует200 полного файла.304 без validated cache — ошибка, неskippedUnchanged. Partial failures сохраняют дату последнего успешного update и предыдущий файл, добавляя lastError. Новая downloadGeoipDatabase держит120s deadline через streaming body, ограничивает declared/chunked file64MiB и закрывает непригодное тело. Базы проходят existing Country/ASN validation до atomic swap; формат/layout ReUI не менялись. + +Local test:geoip прошёл: реальный HTTP healthy/304, stalls after headers→AbortError, declared/chunked oversize→reject, recovery; collector invalid/cache/concurrent gates; JWT reader cannotmutate/writer/admin allowed. Backend/contracts build и scoped ESLint прошли.15 unique GeoIP settings/responsive/data-collection E2E прошли25.2s; ошибочная первоначальная команда с несуществующимиspec далаNo tests found и не считалась проверкой. Три автоматически перегенерированных старыхGeoIP PNG возвращены к исходным байтам; чужие GRE/VXLAN changes сохранены. + +Final production source516/compiled1072 matched. В own PostgreSQL/files scope выполнены7 укрупнённых сценариев: actual mirror download обеих MMDB12,380,575bytes, Google8.8.8.8→US/ASN15169, actual conditional304/hash unchanged иconcurrent409, missing-country refetch безETag, corrupt-country refetch, controlled invalid MMDB200/uncached304 сохраняютlastSuccessAt, actual recovery иdisabled background0fetch.12 реальных mirror GET200/304; negative country responses явно контролируемые и не выдаются за native WAN failure. Валидация MMDB не подменялась. Temporary DB/role/container/files удалены, CHRcontacts0/mainDBwrites0 в этом scope. Предварительная сборка с иным downloader отдельно проверялась; повтор не прибавлен кunique приёмке. + +Развёрнут только backend ux-geoip242; frontend ux-backups237 image/container ID неизменны. Backendhealthy; real Portal/live GET GeoIP200 иUI1440/320, обе main bases alreadyloaded, globaloverflow0/pageerrors0, POSTupdate0. Main GeoIP job через UI не запускался; реальная загрузка доказана в isolated scope, а не заявлена обновлением данных приложения. Откатbackend ux-backups237 и приватная прежняя.env сохранены на сервере. + +Протоколы: [mirror/SQL](ux-ui/acceptance/geoip-mirror242.json), [deployment](ux-ui/acceptance/geoip242-deployment.json), [live read UI](ux-ui/acceptance/geoip242-live-ui.json). MirrorP3TERX — фактический источник collector без account key. Прямой licensed MaxMind download не реализован текущим collector и не принят этим тестом; актуальность/геолокационная точность всех IP, publicIPv6 geography, multi-process lock, physical WAN failures отдельно. + +| Референс | Схема состояний | +| --- | --- | +| [ReUI Settings16](https://reui.io/preview/base/settings-16?ref=mcp) | validated cache → conditional304; missing/corrupt → full200 → validate → swap; error → last-good + last-success + warning | + +Следующий243 — read-only availability audit контейнерных/ACME/release jobs на текущем стенде и согласованная матрица реальных remaining dependencies. Не выдумывать Cloudflare/registry credentials и не обновлять RouterOS без отдельной команды. + ## 12.5.241 — тысяча бэкапов со смешанными состояниями файлов (2026-10-14) Изменение237 проверено на1000 обычных live-path HTTP DTO (199062bytes), по250 local/missing,both/missing,local/unknown,local/available. Два сценария1440/320 прошли25.2s. UI сохраняет pagination50 строк:DOM2477/2239, ready1203/806ms в dev fixture окружении. Это измерение конкретного запуска, не production SLA/сетевой latency и не доказательство скорости filesystem API1000 файлов. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index ad3a1d9e..51f61ca5 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-14:241 — после237 приняты1000 смешанных local/missing,both/missing,unknown,available строк:ПК1440/320, rendered50, DOM2477/2239, search/last-good/pagination/keyboard passed, scoped axe0, writes0. Runtime backend/frontend ux-backups237. S3 native/limitedUI/retention230–240 приняты в записанных границах. Следующий242 — доступный mirror GeoIP job в изолированном scope; paid MaxMind/Cloudflare/Gitea/container jobs отдельно. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:242 — GeoIP mirror job принят в own SQL/files и реальном mirror:12 fetches200/304, две валидные MMDB12,380,575bytes, missing/corrupt recovery, concurrent409, partial error сохраняетlastSuccessAt. Исправлены false304/eager gate/body deadline; download120s/64MiB, HTTP boundary tests и15E2E прошли. Backend ux-geoip242 healthy, frontend ux-backups237 image/container unchanged; live readUI1440/320 passed. Следующий243 — аудит доступных container/certificate/release jobs и актуальных внешних ограничений. Этап12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -38,6 +38,24 @@ | 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 | | 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка12.3.83 в границах журнала. Внешняя12.5: блоки1–236 завершены в записанных scopes, этап открыт. Runtime backend ux-backup234/frontend ux-routing218; backend25/UI52 и дополнительные SQL/UDP222 прошли. | +## 12.5.242 — реальный GeoIP mirror job и честное состояние cache (2026-10-14) + +В collector обнаруженыfalse-success304 при отсутствующей локальной базе (старый тест прямо принимал это), race до acquisition lock из-за await settings и освобождение timeout после headers вместо body. Теперь gate захватывается до первогоawait; concurrent force409/backgroundskip. Conditional ETag отправляется только после проверки локальной MMDB; missing/corrupt cache требует200 полного файла.304 без validated cache — ошибка, неskippedUnchanged. Partial failures сохраняют дату последнего успешного update и предыдущий файл, добавляя lastError. Новая downloadGeoipDatabase держит120s deadline через streaming body, ограничивает declared/chunked file64MiB и закрывает непригодное тело. Базы проходят existing Country/ASN validation до atomic swap; формат/layout ReUI не менялись. + +Local test:geoip прошёл: реальный HTTP healthy/304, stalls after headers→AbortError, declared/chunked oversize→reject, recovery; collector invalid/cache/concurrent gates; JWT reader cannotmutate/writer/admin allowed. Backend/contracts build и scoped ESLint прошли.15 unique GeoIP settings/responsive/data-collection E2E прошли25.2s; ошибочная первоначальная команда с несуществующимиspec далаNo tests found и не считалась проверкой. Три автоматически перегенерированных старыхGeoIP PNG возвращены к исходным байтам; чужие GRE/VXLAN changes сохранены. + +Final production source516/compiled1072 matched. В own PostgreSQL/files scope выполнены7 укрупнённых сценариев: actual mirror download обеих MMDB12,380,575bytes, Google8.8.8.8→US/ASN15169, actual conditional304/hash unchanged иconcurrent409, missing-country refetch безETag, corrupt-country refetch, controlled invalid MMDB200/uncached304 сохраняютlastSuccessAt, actual recovery иdisabled background0fetch.12 реальных mirror GET200/304; negative country responses явно контролируемые и не выдаются за native WAN failure. Валидация MMDB не подменялась. Temporary DB/role/container/files удалены, CHRcontacts0/mainDBwrites0 в этом scope. Предварительная сборка с иным downloader отдельно проверялась; повтор не прибавлен кunique приёмке. + +Развёрнут только backend ux-geoip242; frontend ux-backups237 image/container ID неизменны. Backendhealthy; real Portal/live GET GeoIP200 иUI1440/320, обе main bases alreadyloaded, globaloverflow0/pageerrors0, POSTupdate0. Main GeoIP job через UI не запускался; реальная загрузка доказана в isolated scope, а не заявлена обновлением данных приложения. Откатbackend ux-backups237 и приватная прежняя.env сохранены на сервере. + +Протоколы: [mirror/SQL](ux-ui/acceptance/geoip-mirror242.json), [deployment](ux-ui/acceptance/geoip242-deployment.json), [live read UI](ux-ui/acceptance/geoip242-live-ui.json). MirrorP3TERX — фактический источник collector без account key. Прямой licensed MaxMind download не реализован текущим collector и не принят этим тестом; актуальность/геолокационная точность всех IP, publicIPv6 geography, multi-process lock, physical WAN failures отдельно. + +| Референс | Схема состояний | +| --- | --- | +| [ReUI Settings16](https://reui.io/preview/base/settings-16?ref=mcp) | validated cache → conditional304; missing/corrupt → full200 → validate → swap; error → last-good + last-success + warning | + +Следующий243 — read-only availability audit контейнерных/ACME/release jobs на текущем стенде и согласованная матрица реальных remaining dependencies. Не выдумывать Cloudflare/registry credentials и не обновлять RouterOS без отдельной команды. + ## 12.5.241 — тысяча бэкапов со смешанными состояниями файлов (2026-10-14) Изменение237 проверено на1000 обычных live-path HTTP DTO (199062bytes), по250 local/missing,both/missing,local/unknown,local/available. Два сценария1440/320 прошли25.2s. UI сохраняет pagination50 строк:DOM2477/2239, ready1203/806ms в dev fixture окружении. Это измерение конкретного запуска, не production SLA/сетевой latency и не доказательство скорости filesystem API1000 файлов. diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index 88a0c65a..18352811 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,10 +1,28 @@ # Оставшаяся внешняя приёмка — 2026-10-14 -Актуальное продолжение 2026-10-14:241 — после237 приняты1000 смешанных local/missing,both/missing,unknown,available строк:ПК1440/320, rendered50, DOM2477/2239, search/last-good/pagination/keyboard passed, scoped axe0, writes0. Runtime backend/frontend ux-backups237. S3 native/limitedUI/retention230–240 приняты в записанных границах. Следующий242 — доступный mirror GeoIP job в изолированном scope; paid MaxMind/Cloudflare/Gitea/container jobs отдельно. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:242 — GeoIP mirror job принят в own SQL/files и реальном mirror:12 fetches200/304, две валидные MMDB12,380,575bytes, missing/corrupt recovery, concurrent409, partial error сохраняетlastSuccessAt. Исправлены false304/eager gate/body deadline; download120s/64MiB, HTTP boundary tests и15E2E прошли. Backend ux-geoip242 healthy, frontend ux-backups237 image/container unchanged; live readUI1440/320 passed. Следующий243 — аудит доступных container/certificate/release jobs и актуальных внешних ограничений. Этап12.5 открыт. -Это актуальная матрица после 12.5.241. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.242. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. -Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-backups237`, frontend `ux-backups237`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. +Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-geoip242`, frontend `ux-backups237`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись. + +## 12.5.242 — реальный GeoIP mirror job и честное состояние cache (2026-10-14) + +В collector обнаруженыfalse-success304 при отсутствующей локальной базе (старый тест прямо принимал это), race до acquisition lock из-за await settings и освобождение timeout после headers вместо body. Теперь gate захватывается до первогоawait; concurrent force409/backgroundskip. Conditional ETag отправляется только после проверки локальной MMDB; missing/corrupt cache требует200 полного файла.304 без validated cache — ошибка, неskippedUnchanged. Partial failures сохраняют дату последнего успешного update и предыдущий файл, добавляя lastError. Новая downloadGeoipDatabase держит120s deadline через streaming body, ограничивает declared/chunked file64MiB и закрывает непригодное тело. Базы проходят existing Country/ASN validation до atomic swap; формат/layout ReUI не менялись. + +Local test:geoip прошёл: реальный HTTP healthy/304, stalls after headers→AbortError, declared/chunked oversize→reject, recovery; collector invalid/cache/concurrent gates; JWT reader cannotmutate/writer/admin allowed. Backend/contracts build и scoped ESLint прошли.15 unique GeoIP settings/responsive/data-collection E2E прошли25.2s; ошибочная первоначальная команда с несуществующимиspec далаNo tests found и не считалась проверкой. Три автоматически перегенерированных старыхGeoIP PNG возвращены к исходным байтам; чужие GRE/VXLAN changes сохранены. + +Final production source516/compiled1072 matched. В own PostgreSQL/files scope выполнены7 укрупнённых сценариев: actual mirror download обеих MMDB12,380,575bytes, Google8.8.8.8→US/ASN15169, actual conditional304/hash unchanged иconcurrent409, missing-country refetch безETag, corrupt-country refetch, controlled invalid MMDB200/uncached304 сохраняютlastSuccessAt, actual recovery иdisabled background0fetch.12 реальных mirror GET200/304; negative country responses явно контролируемые и не выдаются за native WAN failure. Валидация MMDB не подменялась. Temporary DB/role/container/files удалены, CHRcontacts0/mainDBwrites0 в этом scope. Предварительная сборка с иным downloader отдельно проверялась; повтор не прибавлен кunique приёмке. + +Развёрнут только backend ux-geoip242; frontend ux-backups237 image/container ID неизменны. Backendhealthy; real Portal/live GET GeoIP200 иUI1440/320, обе main bases alreadyloaded, globaloverflow0/pageerrors0, POSTupdate0. Main GeoIP job через UI не запускался; реальная загрузка доказана в isolated scope, а не заявлена обновлением данных приложения. Откатbackend ux-backups237 и приватная прежняя.env сохранены на сервере. + +Протоколы: [mirror/SQL](ux-ui/acceptance/geoip-mirror242.json), [deployment](ux-ui/acceptance/geoip242-deployment.json), [live read UI](ux-ui/acceptance/geoip242-live-ui.json). MirrorP3TERX — фактический источник collector без account key. Прямой licensed MaxMind download не реализован текущим collector и не принят этим тестом; актуальность/геолокационная точность всех IP, publicIPv6 geography, multi-process lock, physical WAN failures отдельно. + +| Референс | Схема состояний | +| --- | --- | +| [ReUI Settings16](https://reui.io/preview/base/settings-16?ref=mcp) | validated cache → conditional304; missing/corrupt → full200 → validate → swap; error → last-good + last-success + warning | + +Следующий243 — read-only availability audit контейнерных/ACME/release jobs на текущем стенде и согласованная матрица реальных remaining dependencies. Не выдумывать Cloudflare/registry credentials и не обновлять RouterOS без отдельной команды. ## 12.5.241 — тысяча бэкапов со смешанными состояниями файлов (2026-10-14) @@ -81,7 +99,7 @@ Production backend/frontend ux-backups237 собраны и развёрнуты | REST и достоверность данных | Ошибки OSPF/BFD/BGP/GRE/WAN215–221 явные; latest API smoke222 прошёл | Correlated236:384 samples/40 diagnostics без отказов; current policies/address/load исправны.828/304 retained REST sessions и vendor inactive-session fix — гипотеза; 238:763.844s/624samples/24diagnostics все200, API records изменились без отказа; причинность401/500 и version control остаются открытыми | | Клиентские ОС и внешняя сеть | CHR↔CHR/Linux, private IPv4/ULA и synthetic IPv6/IPFIX; Android full-tunnel/reconnect227 подтверждены пользователем | Windows/macOS/iOS VPN, расширенная Android приёмка, независимые WAN/multi-WAN и public IPv6; public IPv6 на текущем стенде отсутствует | | Масштаб и длительная устойчивость | Контролируемые lock/dedupe/loss/recovery и короткие native проверки | Несколько backend/worker/writers, длительный rekey/REST/traffic, physical disconnect и maximum throughput | -| Внешние хранилища и jobs | Локальные API/errors/RBAC/owned SQL; реальные Portal1.3.1/Telegram195–201 и Selectel S3/client/API/ownSQL228–229 | Large native exports проверены HTTP fixture; Missing local files UX237 принят; Bridge-only S3 restore через UI239 принят; Retention240 в own scope принят; миграция между buckets/multi-instance; provider/container/Gitea/MaxMind jobs с соответствующими endpoints/аккаунтами; private/paid credentials не выдумывать | +| Внешние хранилища и jobs | Локальные API/errors/RBAC/owned SQL; реальные Portal1.3.1/Telegram195–201 и Selectel S3/client/API/ownSQL228–229 | Large native exports проверены HTTP fixture; Missing local files UX237 принят; Bridge-only S3 restore через UI239 принят; Retention240 в own scope принят; миграция между buckets/multi-instance; GeoIP mirror242 принят; прямой licensed MaxMind не реализован; provider/container/Gitea/ACME jobs с соответствующими endpoints/аккаунтами; private/paid credentials не выдумывать | | Расширенные сетевые конфигурации | GRE/WG/VXLAN/IPsec/OSPF/BFD/BGP сценарии и точные границы записаны | from-pool/EUI-64/multiple-address import/export, outer IPv6, произвольная PKI/OID/policy/path-length и длительные переходы; BFD-only gate гарантией не стал | | Общий UX, доступность и производительность | Полная UI192 исторически; latest backend25/UI52, scoped contrast/live218 и keyboard206 | Свежая UI226 принята в записанных границах; профильный performance и ручная accessibility/screen-reader приёмка; scoped axe не полный WCAG | | Границы выпуска | План/доказательства/датированные блоки сохранены; коммиты локальные | Определить требуемые конфигурации и принять результат в этих границах; невыполненные варианты остаются явными ограничениями | @@ -108,7 +126,7 @@ Portal offline JWT действует доexp после logout/role change; э ## Следующая последовательность — 2026-10-14 -1. **242 — доступные external jobs:** actual GeoIP mirror без licence key в own SQL/files scope; отдельно сохранять ограничения paid MaxMind/Cloudflare/registry/Gitea. Затем — актуальная матрица границ выпуска. +1. **243 — availability audit:** read-only packages/device-mode/storage/API/config для containers, ACME и releases; проверить реально доступные GET-сценарии, зафиксировать конкретные внешние зависимости. 2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания. 3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций. diff --git a/docs/ux-ui-stage-12.md b/docs/ux-ui-stage-12.md index 375837d3..12ccc4ab 100644 --- a/docs/ux-ui-stage-12.md +++ b/docs/ux-ui-stage-12.md @@ -1,6 +1,6 @@ # Этап 12: итоговая приёмка UX/UI -Актуальное продолжение 2026-10-14:241 — после237 приняты1000 смешанных local/missing,both/missing,unknown,available строк:ПК1440/320, rendered50, DOM2477/2239, search/last-good/pagination/keyboard passed, scoped axe0, writes0. Runtime backend/frontend ux-backups237. S3 native/limitedUI/retention230–240 приняты в записанных границах. Следующий242 — доступный mirror GeoIP job в изолированном scope; paid MaxMind/Cloudflare/Gitea/container jobs отдельно. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:242 — GeoIP mirror job принят в own SQL/files и реальном mirror:12 fetches200/304, две валидные MMDB12,380,575bytes, missing/corrupt recovery, concurrent409, partial error сохраняетlastSuccessAt. Исправлены false304/eager gate/body deadline; download120s/64MiB, HTTP boundary tests и15E2E прошли. Backend ux-geoip242 healthy, frontend ux-backups237 image/container unchanged; live readUI1440/320 passed. Следующий243 — аудит доступных container/certificate/release jobs и актуальных внешних ограничений. Этап12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -4152,6 +4152,24 @@ Real Portal/browser API, provider local verified: создан один job ро Runtime backend ux-backup234/frontend ux-routing218 healthy; новое application image для volume-only fix не требовалось. Пароли CHR не менялись, S3 mutations0 в235, configs body/JWT не опубликованы, private local manifest удалён. Исходные10 missing local files не восстановлены и records не удалены. Force-recreate — не host reboot/power loss/fsync/DR приёмка. Далее236 — UX недоступных local files, затем version retention границы и оставшиеся7 группы матрицы; multi-instance guard не принимается. Этап12.5 открыт. +## 12.5.242 — реальный GeoIP mirror job и честное состояние cache (2026-10-14) + +В collector обнаруженыfalse-success304 при отсутствующей локальной базе (старый тест прямо принимал это), race до acquisition lock из-за await settings и освобождение timeout после headers вместо body. Теперь gate захватывается до первогоawait; concurrent force409/backgroundskip. Conditional ETag отправляется только после проверки локальной MMDB; missing/corrupt cache требует200 полного файла.304 без validated cache — ошибка, неskippedUnchanged. Partial failures сохраняют дату последнего успешного update и предыдущий файл, добавляя lastError. Новая downloadGeoipDatabase держит120s deadline через streaming body, ограничивает declared/chunked file64MiB и закрывает непригодное тело. Базы проходят existing Country/ASN validation до atomic swap; формат/layout ReUI не менялись. + +Local test:geoip прошёл: реальный HTTP healthy/304, stalls after headers→AbortError, declared/chunked oversize→reject, recovery; collector invalid/cache/concurrent gates; JWT reader cannotmutate/writer/admin allowed. Backend/contracts build и scoped ESLint прошли.15 unique GeoIP settings/responsive/data-collection E2E прошли25.2s; ошибочная первоначальная команда с несуществующимиspec далаNo tests found и не считалась проверкой. Три автоматически перегенерированных старыхGeoIP PNG возвращены к исходным байтам; чужие GRE/VXLAN changes сохранены. + +Final production source516/compiled1072 matched. В own PostgreSQL/files scope выполнены7 укрупнённых сценариев: actual mirror download обеих MMDB12,380,575bytes, Google8.8.8.8→US/ASN15169, actual conditional304/hash unchanged иconcurrent409, missing-country refetch безETag, corrupt-country refetch, controlled invalid MMDB200/uncached304 сохраняютlastSuccessAt, actual recovery иdisabled background0fetch.12 реальных mirror GET200/304; negative country responses явно контролируемые и не выдаются за native WAN failure. Валидация MMDB не подменялась. Temporary DB/role/container/files удалены, CHRcontacts0/mainDBwrites0 в этом scope. Предварительная сборка с иным downloader отдельно проверялась; повтор не прибавлен кunique приёмке. + +Развёрнут только backend ux-geoip242; frontend ux-backups237 image/container ID неизменны. Backendhealthy; real Portal/live GET GeoIP200 иUI1440/320, обе main bases alreadyloaded, globaloverflow0/pageerrors0, POSTupdate0. Main GeoIP job через UI не запускался; реальная загрузка доказана в isolated scope, а не заявлена обновлением данных приложения. Откатbackend ux-backups237 и приватная прежняя.env сохранены на сервере. + +Протоколы: [mirror/SQL](ux-ui/acceptance/geoip-mirror242.json), [deployment](ux-ui/acceptance/geoip242-deployment.json), [live read UI](ux-ui/acceptance/geoip242-live-ui.json). MirrorP3TERX — фактический источник collector без account key. Прямой licensed MaxMind download не реализован текущим collector и не принят этим тестом; актуальность/геолокационная точность всех IP, publicIPv6 geography, multi-process lock, physical WAN failures отдельно. + +| Референс | Схема состояний | +| --- | --- | +| [ReUI Settings16](https://reui.io/preview/base/settings-16?ref=mcp) | validated cache → conditional304; missing/corrupt → full200 → validate → swap; error → last-good + last-success + warning | + +Следующий243 — read-only availability audit контейнерных/ACME/release jobs на текущем стенде и согласованная матрица реальных remaining dependencies. Не выдумывать Cloudflare/registry credentials и не обновлять RouterOS без отдельной команды. + ## 12.5.241 — тысяча бэкапов со смешанными состояниями файлов (2026-10-14) Изменение237 проверено на1000 обычных live-path HTTP DTO (199062bytes), по250 local/missing,both/missing,local/unknown,local/available. Два сценария1440/320 прошли25.2s. UI сохраняет pagination50 строк:DOM2477/2239, ready1203/806ms в dev fixture окружении. Это измерение конкретного запуска, не production SLA/сетевой latency и не доказательство скорости filesystem API1000 файлов. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index d581888c..ce52b67e 100644 --- a/docs/ux-ui-test-server.md +++ b/docs/ux-ui-test-server.md @@ -1,6 +1,6 @@ # Тестовый сервер UX/UI — 2026-10-10 -Актуальное продолжение 2026-10-14:241 — после237 приняты1000 смешанных local/missing,both/missing,unknown,available строк:ПК1440/320, rendered50, DOM2477/2239, search/last-good/pagination/keyboard passed, scoped axe0, writes0. Runtime backend/frontend ux-backups237. S3 native/limitedUI/retention230–240 приняты в записанных границах. Следующий242 — доступный mirror GeoIP job в изолированном scope; paid MaxMind/Cloudflare/Gitea/container jobs отдельно. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:242 — GeoIP mirror job принят в own SQL/files и реальном mirror:12 fetches200/304, две валидные MMDB12,380,575bytes, missing/corrupt recovery, concurrent409, partial error сохраняетlastSuccessAt. Исправлены false304/eager gate/body deadline; download120s/64MiB, HTTP boundary tests и15E2E прошли. Backend ux-geoip242 healthy, frontend ux-backups237 image/container unchanged; live readUI1440/320 passed. Следующий243 — аудит доступных container/certificate/release jobs и актуальных внешних ограничений. Этап12.5 открыт. | Параметр | Проверенное состояние | | --- | --- | @@ -34,6 +34,24 @@ OSPF84: real GRE Full adjacency/private routes/Ping3/3 и4 live PC/320px URL/bro BFD85: Up/Full/own route/Ping,4 live UI и loss/withdrawal/recovery приняты; exact own cleanup8 коллекций×2 passed. Withdrawal5,245ms не подтверждает sub-second BFD timing; этот вопрос открыт. Runtime routing83 без новой сборки. [Отчёт](ux-ui/acceptance/ospf-bfd-loss-2026-10-09.json). +## 12.5.242 — реальный GeoIP mirror job и честное состояние cache (2026-10-14) + +В collector обнаруженыfalse-success304 при отсутствующей локальной базе (старый тест прямо принимал это), race до acquisition lock из-за await settings и освобождение timeout после headers вместо body. Теперь gate захватывается до первогоawait; concurrent force409/backgroundskip. Conditional ETag отправляется только после проверки локальной MMDB; missing/corrupt cache требует200 полного файла.304 без validated cache — ошибка, неskippedUnchanged. Partial failures сохраняют дату последнего успешного update и предыдущий файл, добавляя lastError. Новая downloadGeoipDatabase держит120s deadline через streaming body, ограничивает declared/chunked file64MiB и закрывает непригодное тело. Базы проходят existing Country/ASN validation до atomic swap; формат/layout ReUI не менялись. + +Local test:geoip прошёл: реальный HTTP healthy/304, stalls after headers→AbortError, declared/chunked oversize→reject, recovery; collector invalid/cache/concurrent gates; JWT reader cannotmutate/writer/admin allowed. Backend/contracts build и scoped ESLint прошли.15 unique GeoIP settings/responsive/data-collection E2E прошли25.2s; ошибочная первоначальная команда с несуществующимиspec далаNo tests found и не считалась проверкой. Три автоматически перегенерированных старыхGeoIP PNG возвращены к исходным байтам; чужие GRE/VXLAN changes сохранены. + +Final production source516/compiled1072 matched. В own PostgreSQL/files scope выполнены7 укрупнённых сценариев: actual mirror download обеих MMDB12,380,575bytes, Google8.8.8.8→US/ASN15169, actual conditional304/hash unchanged иconcurrent409, missing-country refetch безETag, corrupt-country refetch, controlled invalid MMDB200/uncached304 сохраняютlastSuccessAt, actual recovery иdisabled background0fetch.12 реальных mirror GET200/304; negative country responses явно контролируемые и не выдаются за native WAN failure. Валидация MMDB не подменялась. Temporary DB/role/container/files удалены, CHRcontacts0/mainDBwrites0 в этом scope. Предварительная сборка с иным downloader отдельно проверялась; повтор не прибавлен кunique приёмке. + +Развёрнут только backend ux-geoip242; frontend ux-backups237 image/container ID неизменны. Backendhealthy; real Portal/live GET GeoIP200 иUI1440/320, обе main bases alreadyloaded, globaloverflow0/pageerrors0, POSTupdate0. Main GeoIP job через UI не запускался; реальная загрузка доказана в isolated scope, а не заявлена обновлением данных приложения. Откатbackend ux-backups237 и приватная прежняя.env сохранены на сервере. + +Протоколы: [mirror/SQL](ux-ui/acceptance/geoip-mirror242.json), [deployment](ux-ui/acceptance/geoip242-deployment.json), [live read UI](ux-ui/acceptance/geoip242-live-ui.json). MirrorP3TERX — фактический источник collector без account key. Прямой licensed MaxMind download не реализован текущим collector и не принят этим тестом; актуальность/геолокационная точность всех IP, publicIPv6 geography, multi-process lock, physical WAN failures отдельно. + +| Референс | Схема состояний | +| --- | --- | +| [ReUI Settings16](https://reui.io/preview/base/settings-16?ref=mcp) | validated cache → conditional304; missing/corrupt → full200 → validate → swap; error → last-good + last-success + warning | + +Следующий243 — read-only availability audit контейнерных/ACME/release jobs на текущем стенде и согласованная матрица реальных remaining dependencies. Не выдумывать Cloudflare/registry credentials и не обновлять RouterOS без отдельной команды. + ## 12.5.241 — тысяча бэкапов со смешанными состояниями файлов (2026-10-14) Изменение237 проверено на1000 обычных live-path HTTP DTO (199062bytes), по250 local/missing,both/missing,local/unknown,local/available. Два сценария1440/320 прошли25.2s. UI сохраняет pagination50 строк:DOM2477/2239, ready1203/806ms в dev fixture окружении. Это измерение конкретного запуска, не production SLA/сетевой latency и не доказательство скорости filesystem API1000 файлов. diff --git a/docs/ux-ui/acceptance/geoip-mirror242.json b/docs/ux-ui/acceptance/geoip-mirror242.json new file mode 100644 index 00000000..b3cb2ff0 --- /dev/null +++ b/docs/ux-ui/acceptance/geoip-mirror242.json @@ -0,0 +1,129 @@ +{ + "date": "2026-10-14", + "block": "12.5.242", + "passed": true, + "scope": "Compiled GeoIP collector + owned PostgreSQL/files + actual public mirror; controlled negative bodies separately marked", + "mirrorRequests": [ + { + "file": "GeoLite2-Country.mmdb", + "conditional": false, + "status": 200, + "headersMs": 1112 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": false, + "status": 200, + "headersMs": 1209 + }, + { + "file": "GeoLite2-Country.mmdb", + "conditional": true, + "status": 304, + "headersMs": 97 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": true, + "status": 304, + "headersMs": 126 + }, + { + "file": "GeoLite2-Country.mmdb", + "conditional": false, + "status": 200, + "headersMs": 94 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": true, + "status": 304, + "headersMs": 99 + }, + { + "file": "GeoLite2-Country.mmdb", + "conditional": false, + "status": 200, + "headersMs": 94 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": true, + "status": 304, + "headersMs": 104 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": true, + "status": 304, + "headersMs": 93 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": true, + "status": 304, + "headersMs": 123 + }, + { + "file": "GeoLite2-Country.mmdb", + "conditional": false, + "status": 200, + "headersMs": 102 + }, + { + "file": "GeoLite2-ASN.mmdb", + "conditional": true, + "status": 304, + "headersMs": 90 + } + ], + "checks": [ + { + "name": "actual-two-downloads-valid-mmdb-and-lookup", + "bytes": 12380575, + "hashes": { + "country": "635078e412c4443a2950c2e3f775ccdc400e0a1fea5ffd32e5489e5401a9e51d", + "asn": "7d1cf6cd79a0f35b446570058efb7d05cb6db6dd10bbbc09c3699473f6860cba" + }, + "settings": { + "enabled": false, + "updateIntervalSec": 604800, + "lastCheckAt": "2026-10-14 07:59:58.997+00", + "lastSuccessAt": "2026-10-14 07:59:58.997+00", + "lastError": null, + "countryBuildAt": "2026-10-09 16:52:08+00", + "asnBuildAt": "2026-10-12 08:15:43+00", + "updatedAt": "2026-10-14 08:00:02.048+00" + }, + "lookup": { + "country": "US", + "asn": 15169 + } + }, + { + "name": "actual-conditional304-and-concurrent409", + "skippedUnchanged": 2, + "hashesUnchanged": true + }, + { + "name": "missing-country-refetched-without-conditional-header" + }, + { + "name": "corrupt-local-country-refetched-and-validated" + }, + { + "name": "controlled-invalid-body-preserves-file-and-last-success", + "negativeCountryResponse": "controlled-200-invalid-MMDB", + "realAsnStatus": 304 + }, + { + "name": "controlled-uncached304-is-error-with-last-success-retained" + }, + { + "name": "actual-recovery-and-disabled-background-no-fetch" + } + ], + "chrContacts": 0, + "mainDatabaseWrites": 0, + "ownedDatabaseAndRoleRemoved": true +} diff --git a/docs/ux-ui/acceptance/geoip242-deployment.json b/docs/ux-ui/acceptance/geoip242-deployment.json new file mode 100644 index 00000000..90ce860c --- /dev/null +++ b/docs/ux-ui/acceptance/geoip242-deployment.json @@ -0,0 +1,24 @@ +{ + "date": "2026-10-14", + "block": "12.5.242", + "backendSourceMatched": 516, + "backendCompiledMatched": 1072, + "frontendImageAndContainerUnchanged": true, + "containers": { + "backend": { + "tag": "mmapp-test/backend:ux-geoip242", + "imageId": "sha256:32214bbd6b3df479cf5b503fd370b96abddbe5497fe5fe0fd4b72a009d730a6e", + "containerId": "675dd3412be519a2a92bcd68148db0328264ed784537dd9a3c9e5ce74b33f3ad", + "state": "running", + "health": "healthy" + }, + "frontend": { + "tag": "mmapp-test/frontend:ux-backups237", + "imageId": "sha256:baf9989cde6527715890e608c2c3271c5ea40dfb235ee47a48e21939b0ec1fd0", + "containerId": "dc078016b263444b4e15d3445d8ce5b23c9d93e9707e797d63744e5add881e39", + "state": "running", + "health": null + } + }, + "rollbackBackend": "ux-backups237" +} diff --git a/docs/ux-ui/acceptance/geoip242-live-ui.json b/docs/ux-ui/acceptance/geoip242-live-ui.json new file mode 100644 index 00000000..17e4d46a --- /dev/null +++ b/docs/ux-ui/acceptance/geoip242-live-ui.json @@ -0,0 +1,25 @@ +{ + "date": "2026-10-14", + "block": "12.5.242", + "realPortalLogin": true, + "noMocks": true, + "apiStatus": 200, + "ready": true, + "countryLoaded": true, + "asnLoaded": true, + "views": [ + { + "width": 1440, + "globalOverflow": false, + "geoipSectionVisible": true + }, + { + "width": 320, + "globalOverflow": false, + "geoipSectionVisible": true + } + ], + "pageErrors": [], + "geoipUpdates": 0, + "uiDidNotRequestGeoipUpdate": true +}