diff --git a/backend/scripts/acceptance/s3-ui-limited-restore.cjs b/backend/scripts/acceptance/s3-ui-limited-restore.cjs new file mode 100644 index 00000000..dc0e2919 --- /dev/null +++ b/backend/scripts/acceptance/s3-ui-limited-restore.cjs @@ -0,0 +1,66 @@ +const fs = require('fs'), assert = require('node:assert/strict'), { createHash } = require('crypto'); +const { chromium, expect } = require('@playwright/test'); +(async () => { + const phase = process.argv[2], portal = JSON.parse(fs.readFileSync('tmp/portal195-private.json')); + const state = JSON.parse(fs.readFileSync('tmp/state239-public.json')); + const browser = await chromium.launch({ channel: 'msedge', headless: true }); + try { + const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } }), errors = [], receipts = []; + page.on('pageerror', e => errors.push(e.message)); + page.on('response', r => { if (new URL(r.url()).pathname.startsWith('/api/backups') && r.request().method() !== 'GET') receipts.push({ method: r.request().method(), path: new URL(r.url()).pathname, status: r.status() }); }); + await page.goto('http://localhost:3000/backups'); + await page.getByLabel('Email', { exact: true }).fill(portal.email); + await page.getByLabel('Пароль', { exact: true }).fill(portal.password); + await page.getByRole('button', { name: 'Войти', exact: true }).click(); + await expect(page.getByRole('heading', { name: 'Резервные копии', exact: true })).toBeVisible({ timeout: 30000 }); + if (phase === 'configure') { + const s = JSON.parse(fs.readFileSync('tmp/s3-ui239-private.json')); + const paused = await page.evaluate(async () => { + const r = await fetch('/api/backups/schedule', { method: 'PUT', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + localStorage.getItem('mmapp_token') }, body: JSON.stringify({ enabled: false }) }); + return { status: r.status, body: await r.json() }; + }); + assert.equal(paused.status, 200); assert.equal(paused.body.enabled, false); + await page.getByRole('tab', { name: 'Настройки', exact: true }).click(); + await page.getByRole('button', { name: 'S3', exact: true }).click(); + for (const [label, value] of Object.entries({ Endpoint: s.endpoint, Region: s.region, Bucket: s.bucket, Prefix: state.prefix, 'Access key': s.accessKey, 'Secret key': s.secretKey })) await page.getByLabel(label, { exact: true }).fill(value); + const path = page.getByRole('switch', { name: 'Path-style', exact: true }); + if (await path.getAttribute('aria-checked') === 'true') await path.click(); + const saved = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/storage' && r.request().method() === 'PUT'); + await page.getByRole('button', { name: 'Сохранить хранилище', exact: true }).click(); + assert.equal((await saved).status(), 200); + await expect(page.getByLabel('Secret key', { exact: true })).toHaveValue(''); + const tested = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/storage/test' && r.request().method() === 'POST'); + await page.getByRole('button', { name: 'Проверить', exact: true }).click(); + assert.equal((await tested).status(), 200); + } else { + await page.getByRole('button', { name: 'Обновить', exact: true }).click(); + await expect(page.getByText(state.filename, { exact: true })).toBeVisible(); + if (phase === 'restore') { + const download = page.waitForEvent('download'); + await page.getByRole('button', { name: 'Скачать ' + state.filename, exact: true }).click(); + const d = await download; assert.equal(d.suggestedFilename(), state.filename); + const bytes = fs.readFileSync(await d.path()); + const expected = Buffer.from(`# Own limited UI restore 239\n/interface bridge add name=${state.bridge} comment=mm-ui239\n`); + assert.deepEqual(bytes, expected); + await page.getByRole('button', { name: 'Восстановить ' + state.filename, exact: true }).click(); + const dialog = page.getByRole('alertdialog'); + await expect(dialog).toContainText(state.filename); + const restored = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/' + state.id + '/restore' && r.request().method() === 'POST'); + await dialog.getByRole('button', { name: 'Восстановить', exact: true }).click(); + const r = await restored; assert.equal(r.status(), 200); + const result = await r.json(); assert.equal(result.backupId, state.id); assert.equal(result.serverId, state.serverId); + await expect(dialog).toBeHidden(); + fs.writeFileSync('docs/ux-ui/acceptance/s3-ui239-browser.json', JSON.stringify({ date: '2026-10-14', block: '12.5.239', realPortalLogin: true, noMocks: true, passed: true, downloadBytes: bytes.length, downloadSha256: createHash('sha256').update(bytes).digest('hex'), restoreStatus: 200, restoreReceiptMatched: true, receipts, pageErrors: errors, fullConfigurationApplied: false }, null, 2) + '\n'); + } else if (phase === 'delete') { + await page.getByRole('button', { name: 'Удалить ' + state.filename, exact: true }).click(); + const deleted = page.waitForResponse(r => new URL(r.url()).pathname === '/api/backups/' + state.id && r.request().method() === 'DELETE'); + await page.getByRole('alertdialog').getByRole('button', { name: 'Удалить', exact: true }).click(); + assert.equal((await deleted).status(), 204); await expect(page.getByRole('alertdialog')).toBeHidden(); + await expect(page.getByText(state.filename, { exact: true })).toHaveCount(0); + const p = 'docs/ux-ui/acceptance/s3-ui239-browser.json', report = JSON.parse(fs.readFileSync(p)); + Object.assign(report, { deleteStatus: 204, ownRowRemovedThroughUi: true, deleteReceipts: receipts }); fs.writeFileSync(p, JSON.stringify(report, null, 2) + '\n'); + } else throw Error('Unsupported phase'); + } + assert.deepEqual(errors, []); console.log('S3 UI239 ' + phase + ' passed'); + } finally { await browser.close(); } +})().catch(e => { console.error(e.message); process.exitCode = 1; }); diff --git a/backend/scripts/acceptance/s3-ui-limited-restore.mjs b/backend/scripts/acceptance/s3-ui-limited-restore.mjs new file mode 100644 index 00000000..984cece4 --- /dev/null +++ b/backend/scripts/acceptance/s3-ui-limited-restore.mjs @@ -0,0 +1,89 @@ +import fs from 'node:fs'; +import assert from 'node:assert/strict'; +import { randomUUID, createHash } from 'node:crypto'; +import { createRequire } from 'node:module'; +const require = createRequire('/app/backend/package.json'); +const { ListObjectVersionsCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3'); +const { pool } = await import('/app/backend/dist/db/index.js'); +const { MikrotikClient } = await import('/app/backend/dist/services/mikrotik.js'); +const { createS3ClientFromConfig, s3PutObject, s3GetObject } = await import('/app/backend/dist/services/s3-backup-client.js'); +const phase = process.argv[2], statePath = '/checks/state239-private.json', publicPath = '/checks/s3-ui239-native.json'; +const config = { endpoint: process.env.MMAPP_TEST_S3_ENDPOINT, region: process.env.MMAPP_TEST_S3_REGION, bucket: process.env.MMAPP_TEST_S3_BUCKET, accessKeyId: process.env.MMAPP_TEST_S3_ACCESS_KEY, secretAccessKey: process.env.MMAPP_TEST_S3_SECRET_KEY, forcePathStyle: false }; +assert.equal(config.endpoint, 'https://s3.ru-1.storage.selcloud.ru'); +assert.equal(config.bucket, 'mikrotik-test-s3'); assert(config.secretAccessKey); +const s3 = createS3ClientFromConfig(config); +const server = (await pool.query("SELECT * FROM servers WHERE host='194.226.121.150' AND enabled=true")).rows; +assert.equal(server.length, 1); +const client = new MikrotikClient({ host: server[0].host, port: server[0].port, useSsl: server[0].use_ssl, verifySsl: false, username: server[0].username, password: server[0].password }); +const rows = async table => (await pool.query(`SELECT to_jsonb(${table}) AS row FROM ${table} ORDER BY id`)).rows.map(r => r.row); +const read = endpoint => client.get(endpoint, 10000); +const save = state => fs.writeFileSync(statePath, JSON.stringify(state), { mode: 0o600 }); +async function versions(state) { + const result = []; let keyMarker, versionMarker; + for (let n = 0; n < 10; n++) { + const r = await s3.send(new ListObjectVersionsCommand({ Bucket: config.bucket, Prefix: state.prefix + '/', KeyMarker: keyMarker, VersionIdMarker: versionMarker })); + result.push(...(r.Versions ?? []).map(v => ({ ...v, kind: 'version' })), ...(r.DeleteMarkers ?? []).map(v => ({ ...v, kind: 'delete-marker' }))); + if (!r.IsTruncated) return result; + assert(r.NextKeyMarker || r.NextVersionIdMarker); keyMarker = r.NextKeyMarker; versionMarker = r.NextVersionIdMarker; + } throw Error('Own version pagination limit'); +} +async function restoreTable(table, original) { + assert.equal(original.length, 1); assert.equal(original[0].id, 1); + const columns = Object.keys(original[0]).filter(k => k !== 'id'); + assert(columns.every(k => /^[a-z][a-z0-9_]*$/.test(k))); + await pool.query(`UPDATE ${table} SET ${columns.map((k, n) => `"${k}"=$${n + 1}`).join(',')} WHERE id=1`, columns.map(k => { + const value = original[0][k]; return value && typeof value === 'object' ? JSON.stringify(value) : value; + })); + assert.deepEqual(await rows(table), original); +} +try { + if (phase === 'init') { + assert(!fs.existsSync(statePath)); + const uuid = randomUUID(), bridge = 'mm-ui239-' + uuid.slice(0, 12), filename = 'mm-ui239-' + uuid + '.rsc'; + const state = { id: 'mm-ui239-' + uuid, prefix: 'mmapp-acceptance/' + uuid, bridge, filename, key: 'mmapp-acceptance/' + uuid + '/' + filename, serverId: Number(server[0].id), serverName: server[0].name, storage: await rows('backup_storage_settings'), schedule: await rows('backup_schedule_settings'), catalog: await rows('backup_entries'), bridges: await read('/interface/bridge'), files: await read('/file') }; + assert.equal(state.storage[0].provider, 'local'); assert(state.catalog.every(r => r.storage === 'local' && !r.s3_key)); + assert(!state.bridges.some(b => b.name === bridge)); assert(!state.files.some(f => f.name === filename)); + assert.equal((await versions(state)).length, 0); save(state); + fs.writeFileSync('/checks/state239-public.json', JSON.stringify({ id: state.id, prefix: state.prefix, filename, bridge, serverId: state.serverId })); + console.log('Own scope initialized; original settings/catalog saved privately'); + } else { + const state = JSON.parse(fs.readFileSync(statePath)); + assert(/^mm-ui239-[a-f0-9-]+\.rsc$/.test(state.filename)); assert(state.key === state.prefix + '/' + state.filename); + if (phase === 'upload') { + const storage = (await rows('backup_storage_settings'))[0]; + assert.equal(storage.provider, 's3'); assert.equal(storage.s3_prefix, state.prefix); assert.equal(storage.s3_bucket, config.bucket); + assert.equal((await rows('backup_schedule_settings'))[0].enabled, false); + const script = Buffer.from(`# Own limited UI restore 239\n/interface bridge add name=${state.bridge} comment=mm-ui239\n`); + assert.equal((await versions(state)).length, 0); + await s3PutObject(s3, config.bucket, state.key, script); + assert.deepEqual(await s3GetObject(s3, config.bucket, state.key), script); + await pool.query('INSERT INTO backup_entries(id,server_id,server_name,filename,size_bytes,kind,notes,storage,s3_key,created_at) VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,now())', [state.id, state.serverId, state.serverName, state.filename, script.length, 'manual', 'Own limited UI restore 239: bridge only', 's3', state.key]); + state.bytes = script.length; state.sha256 = createHash('sha256').update(script).digest('hex'); save(state); + console.log(JSON.stringify({ ownS3ScriptUploaded: true, bytes: state.bytes, catalogRowCreated: true })); + } else if (phase === 'verify') { + const bridges = await read('/interface/bridge'), own = bridges.filter(b => b.name === state.bridge); + assert.equal(own.length, 1); assert.equal(own[0].comment, 'mm-ui239'); + assert.equal((await read('/interface/bridge/port')).filter(p => p.bridge === state.bridge).length, 0); + assert.equal((await read('/ip/address')).filter(p => p.interface === state.bridge).length, 0); + assert.deepEqual(bridges.filter(b => b.name !== state.bridge), state.bridges); + state.nativeVerified = true; save(state); + console.log('Native bridge confirmed; no ports/addresses, original bridges unchanged'); + } else if (phase === 'cleanup') { + const own = (await read('/interface/bridge')).filter(b => b.name === state.bridge); + assert(own.length <= 1); + if (own.length) { assert.equal(own[0].comment, 'mm-ui239'); await client.delete('/interface/bridge/' + encodeURIComponent(own[0]['.id'])); } + const ownFiles = (await read('/file')).filter(f => f.name === state.filename); assert(ownFiles.length <= 1); + for (const file of ownFiles) await client.removeFile(file['.id']); + await pool.query('DELETE FROM backup_entries WHERE id=$1 AND filename=$2 AND s3_key=$3', [state.id, state.filename, state.key]); + const existing = await versions(state); assert(existing.every(v => v.Key === state.key && v.VersionId)); + for (const v of existing) await s3.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: v.Key, VersionId: v.VersionId })); + assert.equal((await versions(state)).length, 0); + assert.deepEqual(await rows('backup_entries'), state.catalog); + assert.deepEqual(await read('/interface/bridge'), state.bridges); + assert(!(await read('/file')).some(f => f.name === state.filename)); + await restoreTable('backup_storage_settings', state.storage); await restoreTable('backup_schedule_settings', state.schedule); + const report = { date: '2026-10-14', block: '12.5.239', host: server[0].host, prefix: state.prefix, limitedScriptBytes: state.bytes ?? null, sha256: state.sha256 ?? null, nativeBridgeVerified: Boolean(state.nativeVerified), bridgeAndUploadedFileRemoved: true, originalBridgesUnchanged: true, originalCatalogAllFieldsMatched: true, originalCatalogRows: state.catalog.length, originalSettingsAllFieldsMatched: true, s3VersionsAndMarkersRemoved: existing.length, s3VersionsAndMarkersRemaining: 0, passwordChanges: 0, fullConfigurationApplied: false }; + fs.writeFileSync(publicPath, JSON.stringify(report, null, 2) + '\n'); fs.unlinkSync(statePath); console.log(JSON.stringify(report)); + } else throw Error('Unsupported phase'); + } +} finally { s3.destroy(); await pool.end(); } diff --git a/docs/ux-ui-external-acceptance.md b/docs/ux-ui-external-acceptance.md index 992ea3ef..6bb3ad98 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:238 — REST наблюдался763.844s:624 paired samples +24 diagnostics, все200. На обоих CHR изменились API session records; retained REST rows844→846/320→322. Это не доказывает причину прежних401/500 или точный механизм cached-login renewal. Runtime backend/frontend ux-backups237; missing-file UX237 принят. Следующий239 — S3 restore через live UI в пределах собственного bridge и retention. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:239 — Selectel S3 restore через реальный Portal/UI принят для собственного95-byte скрипта bridge-only:download SHA256 совпал,restore200/receipt и native bridge без ports/IP подтверждены,delete204. Bridge/file/catalog/S3versions очищены; исходные10 строк, storage/schedule сверены целиком и восстановлены; приватные ключи/SQL-снимок удалены. Runtime backend/frontend ux-backups237. Следующий240 — retention в изолированном собственном S3 scope. Этап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,22 @@ 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.239 — восстановление из Selectel S3 через реальный интерфейс (2026-10-14) + +Пользователь явно разрешил передачу тестовых S3 access/secret keys на Debian после отказа automatic approval review. Ключи находились только в приватных ignored/0600 файлах и временных настройках приложения. Перед изменением сохранены raw SQL storage/schedule и10 исходных backup rows приватно. Расписание временно остановлено, virtual-hosted S3 сохранён и проверен через live UI с реальным Portal1.3.1/JWT. Использован новый UUID prefix и ровно один собственный95-byte .rsc: добавление bridge mm-ui239-* с comment=mm-ui239 без портов/адресов. Это специально ограниченный скрипт, не native full export и не полное восстановление CHR. + +Собственный объект записан и прочитан SDK, затем только его строка добавлена в каталог. Через обычный UI скачан файл с совпавшими байтами/SHA256; restore вернул HTTP200 с нужными backupId/serverId. Direct RouterOS на194.226.121.150 подтвердил ровно один bridge, comment, отсутствие bridge ports/IP addresses и неизменность исходных bridges. Через UI удалена только собственная строка/объект:HTTP204, строка исчезла; pageerrors0. Восстановление на втором CHR и полный config restore этим блоком не проверялись. + +Cleanup удалил свой bridge и загруженный .rsc, все версии/delete markers только точного ключа UUID prefix, сверил исходные10 SQL-строк и оба settings rows целиком. Первая попытка остановилась уже после удаления объектов: проверочный SQL-column guard не допускал цифру3 в s3_*; исправлен allowlist имён, повторная очистка прошла. Final-attempt removed0 означает, что версии удалены первой попыткой; конечный listing0 подтверждён. Общее число удалённых версий первой попытки не записано и не выдумывается. Bucket settings, чужие объекты и пароли не менялись. Приватный SQL-снимок и временные ключевые файлы workstation/Debian удалены; production-код и образы ux-backups237 не менялись. + +Acceptance helpers s3-ui-limited-restore.mjs/.cjs проходят node --check. [UI-протокол](ux-ui/acceptance/s3-ui239-browser.json) и [native/cleanup](ux-ui/acceptance/s3-ui239-native.json) фиксируют точные границы. Общая регрессия не повторялась: новых runtime изменений нет. + +| Референс | Схема принятого сценария | +| --- | --- | +| [ReUI Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Собственный объект S3 → строка истории → download → подтверждение restore → native bridge → delete → cleanup и исходные настройки | + +Следующий240 — retention в изолированном scope без удаления существующего каталога. Полная конфигурация, массовые экспорты на реальном CHR, миграция buckets, multi-instance, другие ОС/WAN/jobs остаются отдельной приёмкой. + ## 12.5.238 — REST за пределами десятиминутного окна (2026-10-14) Read-only workstation-наблюдение длилось763.844s (07:01:21.663Z–07:14:05.507Z):26 циклов с целевым периодом30s, шесть endpoints × два CHR × compiled MikrotikClient/fetch Connection:close. 624 samples и24 diagnostics, все HTTP200; повторные запросы при отказе не выполнялись, потому что отказов не было. Учётные данные передавались из приватного process env, временный env-файл удалён; Basic/JWT/пароли в публичный отчёт не включены. RouterOS configurationWrites0/passwordChanges0. diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md index 758eabef..ca7b27e8 100644 --- a/docs/ux-ui-plan.md +++ b/docs/ux-ui-plan.md @@ -1,9 +1,25 @@ # План реализации UX/UI по пятичасовым окнам -Актуальное продолжение 2026-10-14:238 — REST наблюдался763.844s:624 paired samples +24 diagnostics, все200. На обоих CHR изменились API session records; retained REST rows844→846/320→322. Это не доказывает причину прежних401/500 или точный механизм cached-login renewal. Runtime backend/frontend ux-backups237; missing-file UX237 принят. Следующий239 — S3 restore через live UI в пределах собственного bridge и retention. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:239 — Selectel S3 restore через реальный Portal/UI принят для собственного95-byte скрипта bridge-only:download SHA256 совпал,restore200/receipt и native bridge без ports/IP подтверждены,delete204. Bridge/file/catalog/S3versions очищены; исходные10 строк, storage/schedule сверены целиком и восстановлены; приватные ключи/SQL-снимок удалены. Runtime backend/frontend ux-backups237. Следующий240 — retention в изолированном собственном S3 scope. Этап12.5 открыт. Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-14. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат. +## 12.5.239 — восстановление из Selectel S3 через реальный интерфейс (2026-10-14) + +Пользователь явно разрешил передачу тестовых S3 access/secret keys на Debian после отказа automatic approval review. Ключи находились только в приватных ignored/0600 файлах и временных настройках приложения. Перед изменением сохранены raw SQL storage/schedule и10 исходных backup rows приватно. Расписание временно остановлено, virtual-hosted S3 сохранён и проверен через live UI с реальным Portal1.3.1/JWT. Использован новый UUID prefix и ровно один собственный95-byte .rsc: добавление bridge mm-ui239-* с comment=mm-ui239 без портов/адресов. Это специально ограниченный скрипт, не native full export и не полное восстановление CHR. + +Собственный объект записан и прочитан SDK, затем только его строка добавлена в каталог. Через обычный UI скачан файл с совпавшими байтами/SHA256; restore вернул HTTP200 с нужными backupId/serverId. Direct RouterOS на194.226.121.150 подтвердил ровно один bridge, comment, отсутствие bridge ports/IP addresses и неизменность исходных bridges. Через UI удалена только собственная строка/объект:HTTP204, строка исчезла; pageerrors0. Восстановление на втором CHR и полный config restore этим блоком не проверялись. + +Cleanup удалил свой bridge и загруженный .rsc, все версии/delete markers только точного ключа UUID prefix, сверил исходные10 SQL-строк и оба settings rows целиком. Первая попытка остановилась уже после удаления объектов: проверочный SQL-column guard не допускал цифру3 в s3_*; исправлен allowlist имён, повторная очистка прошла. Final-attempt removed0 означает, что версии удалены первой попыткой; конечный listing0 подтверждён. Общее число удалённых версий первой попытки не записано и не выдумывается. Bucket settings, чужие объекты и пароли не менялись. Приватный SQL-снимок и временные ключевые файлы workstation/Debian удалены; production-код и образы ux-backups237 не менялись. + +Acceptance helpers s3-ui-limited-restore.mjs/.cjs проходят node --check. [UI-протокол](ux-ui/acceptance/s3-ui239-browser.json) и [native/cleanup](ux-ui/acceptance/s3-ui239-native.json) фиксируют точные границы. Общая регрессия не повторялась: новых runtime изменений нет. + +| Референс | Схема принятого сценария | +| --- | --- | +| [ReUI Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Собственный объект S3 → строка истории → download → подтверждение restore → native bridge → delete → cleanup и исходные настройки | + +Следующий240 — retention в изолированном scope без удаления существующего каталога. Полная конфигурация, массовые экспорты на реальном CHR, миграция buckets, multi-instance, другие ОС/WAN/jobs остаются отдельной приёмкой. + ## 12.5.238 — REST за пределами десятиминутного окна (2026-10-14) Read-only workstation-наблюдение длилось763.844s (07:01:21.663Z–07:14:05.507Z):26 циклов с целевым периодом30s, шесть endpoints × два CHR × compiled MikrotikClient/fetch Connection:close. 624 samples и24 diagnostics, все HTTP200; повторные запросы при отказе не выполнялись, потому что отказов не было. Учётные данные передавались из приватного process env, временный env-файл удалён; Basic/JWT/пароли в публичный отчёт не включены. RouterOS configurationWrites0/passwordChanges0. diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index f08da217..2fb6d60b 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -1,6 +1,6 @@ # UX/UI: журнал реализации -Актуальное продолжение 2026-10-14:238 — REST наблюдался763.844s:624 paired samples +24 diagnostics, все200. На обоих CHR изменились API session records; retained REST rows844→846/320→322. Это не доказывает причину прежних401/500 или точный механизм cached-login renewal. Runtime backend/frontend ux-backups237; missing-file UX237 принят. Следующий239 — S3 restore через live UI в пределах собственного bridge и retention. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:239 — Selectel S3 restore через реальный Portal/UI принят для собственного95-byte скрипта bridge-only:download SHA256 совпал,restore200/receipt и native bridge без ports/IP подтверждены,delete204. Bridge/file/catalog/S3versions очищены; исходные10 строк, storage/schedule сверены целиком и восстановлены; приватные ключи/SQL-снимок удалены. Runtime backend/frontend ux-backups237. Следующий240 — retention в изолированном собственном S3 scope. Этап12.5 открыт. Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md). @@ -38,6 +38,22 @@ | 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.239 — восстановление из Selectel S3 через реальный интерфейс (2026-10-14) + +Пользователь явно разрешил передачу тестовых S3 access/secret keys на Debian после отказа automatic approval review. Ключи находились только в приватных ignored/0600 файлах и временных настройках приложения. Перед изменением сохранены raw SQL storage/schedule и10 исходных backup rows приватно. Расписание временно остановлено, virtual-hosted S3 сохранён и проверен через live UI с реальным Portal1.3.1/JWT. Использован новый UUID prefix и ровно один собственный95-byte .rsc: добавление bridge mm-ui239-* с comment=mm-ui239 без портов/адресов. Это специально ограниченный скрипт, не native full export и не полное восстановление CHR. + +Собственный объект записан и прочитан SDK, затем только его строка добавлена в каталог. Через обычный UI скачан файл с совпавшими байтами/SHA256; restore вернул HTTP200 с нужными backupId/serverId. Direct RouterOS на194.226.121.150 подтвердил ровно один bridge, comment, отсутствие bridge ports/IP addresses и неизменность исходных bridges. Через UI удалена только собственная строка/объект:HTTP204, строка исчезла; pageerrors0. Восстановление на втором CHR и полный config restore этим блоком не проверялись. + +Cleanup удалил свой bridge и загруженный .rsc, все версии/delete markers только точного ключа UUID prefix, сверил исходные10 SQL-строк и оба settings rows целиком. Первая попытка остановилась уже после удаления объектов: проверочный SQL-column guard не допускал цифру3 в s3_*; исправлен allowlist имён, повторная очистка прошла. Final-attempt removed0 означает, что версии удалены первой попыткой; конечный listing0 подтверждён. Общее число удалённых версий первой попытки не записано и не выдумывается. Bucket settings, чужие объекты и пароли не менялись. Приватный SQL-снимок и временные ключевые файлы workstation/Debian удалены; production-код и образы ux-backups237 не менялись. + +Acceptance helpers s3-ui-limited-restore.mjs/.cjs проходят node --check. [UI-протокол](ux-ui/acceptance/s3-ui239-browser.json) и [native/cleanup](ux-ui/acceptance/s3-ui239-native.json) фиксируют точные границы. Общая регрессия не повторялась: новых runtime изменений нет. + +| Референс | Схема принятого сценария | +| --- | --- | +| [ReUI Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Собственный объект S3 → строка истории → download → подтверждение restore → native bridge → delete → cleanup и исходные настройки | + +Следующий240 — retention в изолированном scope без удаления существующего каталога. Полная конфигурация, массовые экспорты на реальном CHR, миграция buckets, multi-instance, другие ОС/WAN/jobs остаются отдельной приёмкой. + ## 12.5.238 — REST за пределами десятиминутного окна (2026-10-14) Read-only workstation-наблюдение длилось763.844s (07:01:21.663Z–07:14:05.507Z):26 циклов с целевым периодом30s, шесть endpoints × два CHR × compiled MikrotikClient/fetch Connection:close. 624 samples и24 diagnostics, все HTTP200; повторные запросы при отказе не выполнялись, потому что отказов не было. Учётные данные передавались из приватного process env, временный env-файл удалён; Basic/JWT/пароли в публичный отчёт не включены. RouterOS configurationWrites0/passwordChanges0. diff --git a/docs/ux-ui-remaining-acceptance.md b/docs/ux-ui-remaining-acceptance.md index e0fb4bb0..359c185c 100644 --- a/docs/ux-ui-remaining-acceptance.md +++ b/docs/ux-ui-remaining-acceptance.md @@ -1,11 +1,27 @@ # Оставшаяся внешняя приёмка — 2026-10-14 -Актуальное продолжение 2026-10-14:238 — REST наблюдался763.844s:624 paired samples +24 diagnostics, все200. На обоих CHR изменились API session records; retained REST rows844→846/320→322. Это не доказывает причину прежних401/500 или точный механизм cached-login renewal. Runtime backend/frontend ux-backups237; missing-file UX237 принят. Следующий239 — S3 restore через live UI в пределах собственного bridge и retention. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:239 — Selectel S3 restore через реальный Portal/UI принят для собственного95-byte скрипта bridge-only:download SHA256 совпал,restore200/receipt и native bridge без ports/IP подтверждены,delete204. Bridge/file/catalog/S3versions очищены; исходные10 строк, storage/schedule сверены целиком и восстановлены; приватные ключи/SQL-снимок удалены. Runtime backend/frontend ux-backups237. Следующий240 — retention в изолированном собственном S3 scope. Этап12.5 открыт. -Это актуальная матрица после 12.5.238. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии. +Это актуальная матрица после 12.5.239. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 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) проверены; пароли не менялись. +## 12.5.239 — восстановление из Selectel S3 через реальный интерфейс (2026-10-14) + +Пользователь явно разрешил передачу тестовых S3 access/secret keys на Debian после отказа automatic approval review. Ключи находились только в приватных ignored/0600 файлах и временных настройках приложения. Перед изменением сохранены raw SQL storage/schedule и10 исходных backup rows приватно. Расписание временно остановлено, virtual-hosted S3 сохранён и проверен через live UI с реальным Portal1.3.1/JWT. Использован новый UUID prefix и ровно один собственный95-byte .rsc: добавление bridge mm-ui239-* с comment=mm-ui239 без портов/адресов. Это специально ограниченный скрипт, не native full export и не полное восстановление CHR. + +Собственный объект записан и прочитан SDK, затем только его строка добавлена в каталог. Через обычный UI скачан файл с совпавшими байтами/SHA256; restore вернул HTTP200 с нужными backupId/serverId. Direct RouterOS на194.226.121.150 подтвердил ровно один bridge, comment, отсутствие bridge ports/IP addresses и неизменность исходных bridges. Через UI удалена только собственная строка/объект:HTTP204, строка исчезла; pageerrors0. Восстановление на втором CHR и полный config restore этим блоком не проверялись. + +Cleanup удалил свой bridge и загруженный .rsc, все версии/delete markers только точного ключа UUID prefix, сверил исходные10 SQL-строк и оба settings rows целиком. Первая попытка остановилась уже после удаления объектов: проверочный SQL-column guard не допускал цифру3 в s3_*; исправлен allowlist имён, повторная очистка прошла. Final-attempt removed0 означает, что версии удалены первой попыткой; конечный listing0 подтверждён. Общее число удалённых версий первой попытки не записано и не выдумывается. Bucket settings, чужие объекты и пароли не менялись. Приватный SQL-снимок и временные ключевые файлы workstation/Debian удалены; production-код и образы ux-backups237 не менялись. + +Acceptance helpers s3-ui-limited-restore.mjs/.cjs проходят node --check. [UI-протокол](ux-ui/acceptance/s3-ui239-browser.json) и [native/cleanup](ux-ui/acceptance/s3-ui239-native.json) фиксируют точные границы. Общая регрессия не повторялась: новых runtime изменений нет. + +| Референс | Схема принятого сценария | +| --- | --- | +| [ReUI Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Собственный объект S3 → строка истории → download → подтверждение restore → native bridge → delete → cleanup и исходные настройки | + +Следующий240 — retention в изолированном scope без удаления существующего каталога. Полная конфигурация, массовые экспорты на реальном CHR, миграция buckets, multi-instance, другие ОС/WAN/jobs остаются отдельной приёмкой. + ## 12.5.238 — REST за пределами десятиминутного окна (2026-10-14) Read-only workstation-наблюдение длилось763.844s (07:01:21.663Z–07:14:05.507Z):26 циклов с целевым периодом30s, шесть endpoints × два CHR × compiled MikrotikClient/fetch Connection:close. 624 samples и24 diagnostics, все HTTP200; повторные запросы при отказе не выполнялись, потому что отказов не было. Учётные данные передавались из приватного process env, временный env-файл удалён; Basic/JWT/пароли в публичный отчёт не включены. RouterOS configurationWrites0/passwordChanges0. @@ -43,7 +59,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 принят; S3 restore через UI, миграция между buckets/multi-instance и retention; 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 принят; миграция между buckets/multi-instance и retention; provider/container/Gitea/MaxMind 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 | | Границы выпуска | План/доказательства/датированные блоки сохранены; коммиты локальные | Определить требуемые конфигурации и принять результат в этих границах; невыполненные варианты остаются явными ограничениями | @@ -70,7 +86,7 @@ Portal offline JWT действует доexp после logout/role change; э ## Следующая последовательность — 2026-10-14 -1. **239 — S3 restore через UI:** только собственный bridge без портов/адресов; отдельный prefix/catalog row, сверка результата и очистки. Затем — retention только собственных объектов. +1. **240 — retention:** изолированные own SQL/S3 scope, сохранение latest copies по каждому серверу, partial cleanup и точная очистка версий. Исходный каталог приложения не использовать для pruning. 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 3328646f..5e4a6ee6 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:238 — REST наблюдался763.844s:624 paired samples +24 diagnostics, все200. На обоих CHR изменились API session records; retained REST rows844→846/320→322. Это не доказывает причину прежних401/500 или точный механизм cached-login renewal. Runtime backend/frontend ux-backups237; missing-file UX237 принят. Следующий239 — S3 restore через live UI в пределах собственного bridge и retention. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:239 — Selectel S3 restore через реальный Portal/UI принят для собственного95-byte скрипта bridge-only:download SHA256 совпал,restore200/receipt и native bridge без ports/IP подтверждены,delete204. Bridge/file/catalog/S3versions очищены; исходные10 строк, storage/schedule сверены целиком и восстановлены; приватные ключи/SQL-снимок удалены. Runtime backend/frontend ux-backups237. Следующий240 — retention в изолированном собственном S3 scope. Этап12.5 открыт. План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push. @@ -4152,6 +4152,22 @@ 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.239 — восстановление из Selectel S3 через реальный интерфейс (2026-10-14) + +Пользователь явно разрешил передачу тестовых S3 access/secret keys на Debian после отказа automatic approval review. Ключи находились только в приватных ignored/0600 файлах и временных настройках приложения. Перед изменением сохранены raw SQL storage/schedule и10 исходных backup rows приватно. Расписание временно остановлено, virtual-hosted S3 сохранён и проверен через live UI с реальным Portal1.3.1/JWT. Использован новый UUID prefix и ровно один собственный95-byte .rsc: добавление bridge mm-ui239-* с comment=mm-ui239 без портов/адресов. Это специально ограниченный скрипт, не native full export и не полное восстановление CHR. + +Собственный объект записан и прочитан SDK, затем только его строка добавлена в каталог. Через обычный UI скачан файл с совпавшими байтами/SHA256; restore вернул HTTP200 с нужными backupId/serverId. Direct RouterOS на194.226.121.150 подтвердил ровно один bridge, comment, отсутствие bridge ports/IP addresses и неизменность исходных bridges. Через UI удалена только собственная строка/объект:HTTP204, строка исчезла; pageerrors0. Восстановление на втором CHR и полный config restore этим блоком не проверялись. + +Cleanup удалил свой bridge и загруженный .rsc, все версии/delete markers только точного ключа UUID prefix, сверил исходные10 SQL-строк и оба settings rows целиком. Первая попытка остановилась уже после удаления объектов: проверочный SQL-column guard не допускал цифру3 в s3_*; исправлен allowlist имён, повторная очистка прошла. Final-attempt removed0 означает, что версии удалены первой попыткой; конечный listing0 подтверждён. Общее число удалённых версий первой попытки не записано и не выдумывается. Bucket settings, чужие объекты и пароли не менялись. Приватный SQL-снимок и временные ключевые файлы workstation/Debian удалены; production-код и образы ux-backups237 не менялись. + +Acceptance helpers s3-ui-limited-restore.mjs/.cjs проходят node --check. [UI-протокол](ux-ui/acceptance/s3-ui239-browser.json) и [native/cleanup](ux-ui/acceptance/s3-ui239-native.json) фиксируют точные границы. Общая регрессия не повторялась: новых runtime изменений нет. + +| Референс | Схема принятого сценария | +| --- | --- | +| [ReUI Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Собственный объект S3 → строка истории → download → подтверждение restore → native bridge → delete → cleanup и исходные настройки | + +Следующий240 — retention в изолированном scope без удаления существующего каталога. Полная конфигурация, массовые экспорты на реальном CHR, миграция buckets, multi-instance, другие ОС/WAN/jobs остаются отдельной приёмкой. + ## 12.5.238 — REST за пределами десятиминутного окна (2026-10-14) Read-only workstation-наблюдение длилось763.844s (07:01:21.663Z–07:14:05.507Z):26 циклов с целевым периодом30s, шесть endpoints × два CHR × compiled MikrotikClient/fetch Connection:close. 624 samples и24 diagnostics, все HTTP200; повторные запросы при отказе не выполнялись, потому что отказов не было. Учётные данные передавались из приватного process env, временный env-файл удалён; Basic/JWT/пароли в публичный отчёт не включены. RouterOS configurationWrites0/passwordChanges0. diff --git a/docs/ux-ui-test-server.md b/docs/ux-ui-test-server.md index 2f02803d..ac071220 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:238 — REST наблюдался763.844s:624 paired samples +24 diagnostics, все200. На обоих CHR изменились API session records; retained REST rows844→846/320→322. Это не доказывает причину прежних401/500 или точный механизм cached-login renewal. Runtime backend/frontend ux-backups237; missing-file UX237 принят. Следующий239 — S3 restore через live UI в пределах собственного bridge и retention. Этап12.5 открыт. +Актуальное продолжение 2026-10-14:239 — Selectel S3 restore через реальный Portal/UI принят для собственного95-byte скрипта bridge-only:download SHA256 совпал,restore200/receipt и native bridge без ports/IP подтверждены,delete204. Bridge/file/catalog/S3versions очищены; исходные10 строк, storage/schedule сверены целиком и восстановлены; приватные ключи/SQL-снимок удалены. Runtime backend/frontend ux-backups237. Следующий240 — retention в изолированном собственном S3 scope. Этап12.5 открыт. | Параметр | Проверенное состояние | | --- | --- | @@ -34,6 +34,22 @@ 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.239 — восстановление из Selectel S3 через реальный интерфейс (2026-10-14) + +Пользователь явно разрешил передачу тестовых S3 access/secret keys на Debian после отказа automatic approval review. Ключи находились только в приватных ignored/0600 файлах и временных настройках приложения. Перед изменением сохранены raw SQL storage/schedule и10 исходных backup rows приватно. Расписание временно остановлено, virtual-hosted S3 сохранён и проверен через live UI с реальным Portal1.3.1/JWT. Использован новый UUID prefix и ровно один собственный95-byte .rsc: добавление bridge mm-ui239-* с comment=mm-ui239 без портов/адресов. Это специально ограниченный скрипт, не native full export и не полное восстановление CHR. + +Собственный объект записан и прочитан SDK, затем только его строка добавлена в каталог. Через обычный UI скачан файл с совпавшими байтами/SHA256; restore вернул HTTP200 с нужными backupId/serverId. Direct RouterOS на194.226.121.150 подтвердил ровно один bridge, comment, отсутствие bridge ports/IP addresses и неизменность исходных bridges. Через UI удалена только собственная строка/объект:HTTP204, строка исчезла; pageerrors0. Восстановление на втором CHR и полный config restore этим блоком не проверялись. + +Cleanup удалил свой bridge и загруженный .rsc, все версии/delete markers только точного ключа UUID prefix, сверил исходные10 SQL-строк и оба settings rows целиком. Первая попытка остановилась уже после удаления объектов: проверочный SQL-column guard не допускал цифру3 в s3_*; исправлен allowlist имён, повторная очистка прошла. Final-attempt removed0 означает, что версии удалены первой попыткой; конечный listing0 подтверждён. Общее число удалённых версий первой попытки не записано и не выдумывается. Bucket settings, чужие объекты и пароли не менялись. Приватный SQL-снимок и временные ключевые файлы workstation/Debian удалены; production-код и образы ux-backups237 не менялись. + +Acceptance helpers s3-ui-limited-restore.mjs/.cjs проходят node --check. [UI-протокол](ux-ui/acceptance/s3-ui239-browser.json) и [native/cleanup](ux-ui/acceptance/s3-ui239-native.json) фиксируют точные границы. Общая регрессия не повторялась: новых runtime изменений нет. + +| Референс | Схема принятого сценария | +| --- | --- | +| [ReUI Data Grid Filtering2](https://reui.io/preview/base/data-grid-filtering-2?ref=mcp), [Sheet8](https://reui.io/preview/base/sheet-8?ref=mcp) | Собственный объект S3 → строка истории → download → подтверждение restore → native bridge → delete → cleanup и исходные настройки | + +Следующий240 — retention в изолированном scope без удаления существующего каталога. Полная конфигурация, массовые экспорты на реальном CHR, миграция buckets, multi-instance, другие ОС/WAN/jobs остаются отдельной приёмкой. + ## 12.5.238 — REST за пределами десятиминутного окна (2026-10-14) Read-only workstation-наблюдение длилось763.844s (07:01:21.663Z–07:14:05.507Z):26 циклов с целевым периодом30s, шесть endpoints × два CHR × compiled MikrotikClient/fetch Connection:close. 624 samples и24 diagnostics, все HTTP200; повторные запросы при отказе не выполнялись, потому что отказов не было. Учётные данные передавались из приватного process env, временный env-файл удалён; Basic/JWT/пароли в публичный отчёт не включены. RouterOS configurationWrites0/passwordChanges0. diff --git a/docs/ux-ui/acceptance/s3-ui239-browser.json b/docs/ux-ui/acceptance/s3-ui239-browser.json new file mode 100644 index 00000000..ec4c6c07 --- /dev/null +++ b/docs/ux-ui/acceptance/s3-ui239-browser.json @@ -0,0 +1,29 @@ +{ + "date": "2026-10-14", + "block": "12.5.239", + "realPortalLogin": true, + "noMocks": true, + "passed": true, + "downloadBytes": 95, + "downloadSha256": "61a84020603fd0aca3818a2cfad630d7b80b1a2506c5211ab4d04874e3459697", + "restoreStatus": 200, + "restoreReceiptMatched": true, + "receipts": [ + { + "method": "POST", + "path": "/api/backups/mm-ui239-6bebaf17-569c-4fe3-9860-dc518ad03364/restore", + "status": 200 + } + ], + "pageErrors": [], + "fullConfigurationApplied": false, + "deleteStatus": 204, + "ownRowRemovedThroughUi": true, + "deleteReceipts": [ + { + "method": "DELETE", + "path": "/api/backups/mm-ui239-6bebaf17-569c-4fe3-9860-dc518ad03364", + "status": 204 + } + ] +} diff --git a/docs/ux-ui/acceptance/s3-ui239-native.json b/docs/ux-ui/acceptance/s3-ui239-native.json new file mode 100644 index 00000000..bd70c833 --- /dev/null +++ b/docs/ux-ui/acceptance/s3-ui239-native.json @@ -0,0 +1,22 @@ +{ + "date": "2026-10-14", + "block": "12.5.239", + "host": "194.226.121.150", + "prefix": "mmapp-acceptance/6bebaf17-569c-4fe3-9860-dc518ad03364", + "limitedScriptBytes": 95, + "sha256": "61a84020603fd0aca3818a2cfad630d7b80b1a2506c5211ab4d04874e3459697", + "nativeBridgeVerified": true, + "bridgeAndUploadedFileRemoved": true, + "originalBridgesUnchanged": true, + "originalCatalogAllFieldsMatched": true, + "originalCatalogRows": 10, + "originalSettingsAllFieldsMatched": true, + "s3VersionsAndMarkersRemaining": 0, + "passwordChanges": 0, + "fullConfigurationApplied": false, + "s3VersionsAndMarkersRemovedInFinalAttempt": 0, + "cleanupAttempts": 2, + "firstCleanupFailure": "Acceptance SQL column guard rejected s3_* names after own object cleanup", + "privateKeyFileRemoved": true, + "privateSqlSnapshotRemoved": true +}