From 2ec1836e8c171913c85aa1bed4d8e36724a451ea Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Fri, 5 Jun 2026 08:55:13 +0300 Subject: [PATCH] chore(architecture): remove dpd portal legacy --- README.md | 1 + adk-rust/Cargo.lock | 11 - adk-rust/Cargo.toml | 1 - adk-rust/crates/detmir-dpd-portal/Cargo.toml | 14 - adk-rust/crates/detmir-dpd-portal/src/main.rs | 366 ------------------ .../detmir-dpd-portal/src/static/app.css | 168 -------- .../detmir-dpd-portal/src/static/app.js | 128 ------ .../detmir-dpd-portal/src/static/index.html | 76 ---- .../detmir-portal/src/contracts/openapi.json | 4 - docs/ARCHITECTURE_BASELINE_RU.md | 58 +++ ...RCHITECTURE_CLEANUP_AUDIT_2026-06-05_RU.md | 46 +++ docs/DPD_PORTAL_RU.md | 127 ------ docs/PORTAL_API_CONTRACTS_RU.md | 11 +- docs/UI_ARCHITECTURE_BASELINE_RU.md | 16 +- 14 files changed, 112 insertions(+), 915 deletions(-) delete mode 100644 adk-rust/crates/detmir-dpd-portal/Cargo.toml delete mode 100644 adk-rust/crates/detmir-dpd-portal/src/main.rs delete mode 100644 adk-rust/crates/detmir-dpd-portal/src/static/app.css delete mode 100644 adk-rust/crates/detmir-dpd-portal/src/static/app.js delete mode 100644 adk-rust/crates/detmir-dpd-portal/src/static/index.html create mode 100644 docs/ARCHITECTURE_BASELINE_RU.md create mode 100644 docs/ARCHITECTURE_CLEANUP_AUDIT_2026-06-05_RU.md delete mode 100644 docs/DPD_PORTAL_RU.md diff --git a/README.md b/README.md index 29bb068..6c60585 100755 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ runtime, OCR/content-analysis, 1C/AI/ETL integration и MCP/dev helpers. Эти - [Release manifest 2026-06](docs/RELEASE_MANIFEST_2026-06.md) - [Эксплуатационный профиль](docs/OPERATIONAL_PROOF_PROFILE_RU.md) - [Коммерческие модули DetMir](docs/DETMIR_COMMERCIAL_MODULES_RU.md) +- [Архитектурный baseline](docs/ARCHITECTURE_BASELINE_RU.md) - [Сторонние компоненты](THIRD_PARTY_COMPONENTS.md) - [Сторонние лицензии](THIRD_PARTY_LICENSES_RU.md) - [Архитектура](docs/ARCHITECTURE_RU.md) diff --git a/adk-rust/Cargo.lock b/adk-rust/Cargo.lock index 9244b18..f4e60f9 100644 --- a/adk-rust/Cargo.lock +++ b/adk-rust/Cargo.lock @@ -563,17 +563,6 @@ dependencies = [ "clap", ] -[[package]] -name = "detmir-dpd-portal" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "reqwest", - "serde_json", - "tiny_http", -] - [[package]] name = "detmir-grafana-check" version = "0.1.0" diff --git a/adk-rust/Cargo.toml b/adk-rust/Cargo.toml index b32e08a..d429e23 100644 --- a/adk-rust/Cargo.toml +++ b/adk-rust/Cargo.toml @@ -28,7 +28,6 @@ members = [ "crates/detmir-grafana-check", "crates/detmir-readiness", "crates/detmir-portal", - "crates/detmir-dpd-portal", "crates/aw-slo-monitor", "crates/aw-rus-healthd", "crates/detmir-check", diff --git a/adk-rust/crates/detmir-dpd-portal/Cargo.toml b/adk-rust/crates/detmir-dpd-portal/Cargo.toml deleted file mode 100644 index caac18f..0000000 --- a/adk-rust/crates/detmir-dpd-portal/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "detmir-dpd-portal" -version = "0.1.0" -edition.workspace = true -rust-version.workspace = true -license.workspace = true -publish.workspace = true - -[dependencies] -anyhow.workspace = true -clap.workspace = true -reqwest.workspace = true -serde_json.workspace = true -tiny_http.workspace = true diff --git a/adk-rust/crates/detmir-dpd-portal/src/main.rs b/adk-rust/crates/detmir-dpd-portal/src/main.rs deleted file mode 100644 index 02a82e7..0000000 --- a/adk-rust/crates/detmir-dpd-portal/src/main.rs +++ /dev/null @@ -1,366 +0,0 @@ -use std::time::Duration; - -use anyhow::{Context, Result, anyhow}; -use clap::Parser; -use reqwest::blocking::Client; -use reqwest::header::{ - CONNECTION, CONTENT_DISPOSITION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, LOCATION, -}; -use serde_json::json; -use tiny_http::{Header, Request, Response, Server, StatusCode}; - -const INDEX_HTML: &str = include_str!("static/index.html"); -const APP_CSS: &str = include_str!("static/app.css"); -const APP_JS: &str = include_str!("static/app.js"); - -#[derive(Clone, Debug, Parser)] -#[command(about = "DetMir DPD parallel portal gateway")] -struct Cli { - #[arg(long, default_value = "127.0.0.1:8722", env = "DETMIR_DPD_BIND")] - bind: String, - - #[arg( - long, - default_value = "http://127.0.0.1:8720", - env = "DETMIR_DPD_UPSTREAM_BASE" - )] - upstream_base: String, - - #[arg(long, default_value_t = 60, env = "DETMIR_DPD_TIMEOUT_SECONDS")] - timeout_seconds: u64, -} - -fn main() { - let code = match run() { - Ok(()) => 0, - Err(err) => { - eprintln!("{err:#}"); - 1 - } - }; - std::process::exit(code); -} - -fn run() -> Result<()> { - let args = Cli::parse(); - let server = Server::http(&args.bind).map_err(|err| anyhow!("bind {}: {err}", args.bind))?; - eprintln!("detmir-dpd-portal listening on http://{}", args.bind); - for request in server.incoming_requests() { - let args = args.clone(); - std::thread::spawn(move || { - if let Err(err) = handle_request(request, &args) { - eprintln!("detmir-dpd-portal request failed: {err:#}"); - } - }); - } - Ok(()) -} - -fn handle_request(request: Request, args: &Cli) -> Result<()> { - let path = normalize_path_with_query(request.url()); - match path_no_query(&path) { - "/_dpd/health" => respond_json_text( - request, - StatusCode(200), - &json!({ - "ok": true, - "portal": "detmir-dpd-portal", - "mode": "parallel-full-mirror", - "upstream": sanitize_upstream(&args.upstream_base), - }) - .to_string(), - ), - "/preview" => respond_redirect(request, "preview/"), - "/preview/" => respond_text( - request, - StatusCode(200), - INDEX_HTML, - "text/html; charset=utf-8", - ), - "/preview/app.css" => { - respond_text(request, StatusCode(200), APP_CSS, "text/css; charset=utf-8") - } - "/preview/app.js" => respond_text( - request, - StatusCode(200), - APP_JS, - "application/javascript; charset=utf-8", - ), - _ => proxy_to_upstream(request, args, &path), - } -} - -fn proxy_to_upstream(mut request: Request, args: &Cli, path: &str) -> Result<()> { - let method = reqwest::Method::from_bytes(request.method().as_str().as_bytes()) - .map_err(|err| anyhow!("unsupported method {}: {err}", request.method()))?; - let forwarded_headers = forwarded_request_headers(&request)?; - let mut body = Vec::new(); - request - .as_reader() - .read_to_end(&mut body) - .context("read request body")?; - - let client = Client::builder() - .timeout(Duration::from_secs(args.timeout_seconds)) - .no_proxy() - .build() - .context("upstream HTTP client")?; - - let url = format!("{}{}", args.upstream_base.trim_end_matches('/'), path); - let mut upstream = client - .request(method, &url) - .header(CONNECTION, HeaderValue::from_static("close")) - .body(body); - for (name, value) in forwarded_headers { - upstream = upstream.header(name, value); - } - - let upstream_response = upstream - .send() - .with_context(|| format!("proxy upstream {url}"))?; - let status = StatusCode(upstream_response.status().as_u16()); - let response_headers = mirrored_response_headers(upstream_response.headers())?; - let mut bytes = upstream_response - .bytes() - .context("upstream response body")? - .to_vec(); - let content_type = response_headers - .iter() - .find(|(name, _)| name.eq_ignore_ascii_case("Content-Type")) - .map(|(_, value)| value.as_str()); - rewrite_mirrored_body(&mut bytes, content_type); - respond_bytes(request, status, bytes, response_headers) -} - -fn forwarded_request_headers(request: &Request) -> Result> { - let mut headers = Vec::new(); - for header in request.headers() { - let name = header.field.as_str(); - let lower = name.to_string().to_ascii_lowercase(); - if is_hop_by_hop_header(&lower) { - continue; - } - if should_forward_header(&lower) { - let name = HeaderName::from_bytes(name.as_bytes()) - .with_context(|| format!("invalid request header name {name}"))?; - headers.push((name, header.value.as_str().to_string())); - } - } - Ok(headers) -} - -fn should_forward_header(lower_name: &str) -> bool { - matches!( - lower_name, - "accept" - | "accept-language" - | "authorization" - | "content-type" - | "cookie" - | "origin" - | "referer" - | "user-agent" - | "x-forwarded-for" - | "x-forwarded-host" - | "x-forwarded-proto" - | "x-gateway-user" - | "x-real-ip" - | "x-remote-user" - ) -} - -fn is_hop_by_hop_header(lower_name: &str) -> bool { - matches!( - lower_name, - "connection" - | "keep-alive" - | "proxy-authenticate" - | "proxy-authorization" - | "te" - | "trailer" - | "transfer-encoding" - | "upgrade" - ) -} - -fn mirrored_response_headers(headers: &HeaderMap) -> Result> { - let mut out = Vec::new(); - for name in [CONTENT_TYPE, CONTENT_DISPOSITION, LOCATION] { - if let Some(value) = headers.get(&name) { - let mut value = value - .to_str() - .with_context(|| format!("invalid upstream {name} header"))? - .to_string(); - if name == LOCATION { - value = rewrite_mirrored_text(&value); - } - out.push((name.as_str().to_string(), value)); - } - } - Ok(out) -} - -fn normalize_path_with_query(url: &str) -> String { - let path = url.trim(); - for prefix in ["/portal-dpd", "/dpd"] { - if let Some(stripped) = path.strip_prefix(prefix) { - return if stripped.is_empty() { - "/".to_string() - } else { - stripped.to_string() - }; - } - } - if path.is_empty() { - "/".to_string() - } else { - path.to_string() - } -} - -fn path_no_query(path: &str) -> &str { - path.split('?').next().unwrap_or(path) -} - -fn sanitize_upstream(value: &str) -> String { - let trimmed = value.trim(); - if trimmed.starts_with("http://127.0.0.1") || trimmed.starts_with("http://localhost") { - "local detmir-portal".to_string() - } else { - "configured upstream".to_string() - } -} - -fn respond_text( - request: Request, - status: StatusCode, - text: &str, - content_type: &str, -) -> Result<()> { - let response = Response::from_string(text.to_string()) - .with_status_code(status) - .with_header( - Header::from_bytes("Content-Type", content_type) - .map_err(|_| anyhow!("invalid Content-Type header"))?, - ) - .with_header( - Header::from_bytes("Cache-Control", "no-store") - .map_err(|_| anyhow!("invalid Cache-Control header"))?, - ); - request.respond(response)?; - Ok(()) -} - -fn respond_json_text(request: Request, status: StatusCode, text: &str) -> Result<()> { - respond_text(request, status, text, "application/json; charset=utf-8") -} - -fn respond_redirect(request: Request, location: &str) -> Result<()> { - let response = Response::from_string("") - .with_status_code(StatusCode(302)) - .with_header( - Header::from_bytes("Location", location) - .map_err(|_| anyhow!("invalid Location header"))?, - ) - .with_header( - Header::from_bytes("Cache-Control", "no-store") - .map_err(|_| anyhow!("invalid Cache-Control header"))?, - ); - request.respond(response)?; - Ok(()) -} - -fn respond_bytes( - request: Request, - status: StatusCode, - body: Vec, - response_headers: Vec<(String, String)>, -) -> Result<()> { - let mut response = Response::from_data(body) - .with_status_code(status) - .with_header( - Header::from_bytes("Cache-Control", "no-store") - .map_err(|_| anyhow!("invalid Cache-Control header"))?, - ); - for (name, value) in response_headers { - response = response.with_header( - Header::from_bytes(name, value) - .map_err(|_| anyhow!("invalid mirrored response header"))?, - ); - } - request.respond(response)?; - Ok(()) -} - -fn rewrite_mirrored_body(body: &mut Vec, content_type: Option<&str>) { - let Some(content_type) = content_type else { - return; - }; - if !(content_type.contains("text/html") || content_type.contains("javascript")) { - return; - } - let Ok(text) = std::str::from_utf8(body) else { - return; - }; - if !text.contains("/portal") { - return; - } - *body = rewrite_mirrored_text(text).into_bytes(); -} - -fn rewrite_mirrored_text(text: &str) -> String { - text.replace("/portal", "/dpd") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalizes_dpd_prefix_and_preserves_query() { - assert_eq!(normalize_path_with_query("/dpd/"), "/"); - assert_eq!( - normalize_path_with_query("/dpd/api/reports?anonymize=1"), - "/api/reports?anonymize=1" - ); - assert_eq!( - normalize_path_with_query("/portal-dpd/api/cases/c1?format=markdown"), - "/api/cases/c1?format=markdown" - ); - } - - #[test] - fn preview_paths_are_not_upstream_mirrored() { - assert_eq!(path_no_query("/preview/app.js?v=1"), "/preview/app.js"); - } - - #[test] - fn rewrites_portal_absolute_paths_for_dpd_mirror() { - let mut body = br#"href="/portal/api/reports"; const x = `/portal/api/cases/c1`;"#.to_vec(); - rewrite_mirrored_body(&mut body, Some("application/javascript; charset=utf-8")); - let text = String::from_utf8(body).unwrap(); - assert!(text.contains("/dpd/api/reports")); - assert!(text.contains("/dpd/api/cases/c1")); - assert!(!text.contains("/portal/api")); - } - - #[test] - fn forwards_operator_identity_headers_but_not_hop_by_hop_headers() { - assert!(should_forward_header("x-remote-user")); - assert!(should_forward_header("x-gateway-user")); - assert!(should_forward_header("authorization")); - assert!(should_forward_header("cookie")); - assert!(!should_forward_header("x-debug-private")); - assert!(is_hop_by_hop_header("connection")); - assert!(is_hop_by_hop_header("transfer-encoding")); - assert!(!is_hop_by_hop_header("x-remote-user")); - } - - #[test] - fn rewrites_portal_locations_for_dpd_mirror() { - assert_eq!( - rewrite_mirrored_text("/portal/reports?format=markdown"), - "/dpd/reports?format=markdown" - ); - } -} diff --git a/adk-rust/crates/detmir-dpd-portal/src/static/app.css b/adk-rust/crates/detmir-dpd-portal/src/static/app.css deleted file mode 100644 index 3c2f3cf..0000000 --- a/adk-rust/crates/detmir-dpd-portal/src/static/app.css +++ /dev/null @@ -1,168 +0,0 @@ -:root { - color-scheme: light; - --bg: #f8fafc; - --panel: #ffffff; - --text: #0f172a; - --muted: #64748b; - --line: #e2e8f0; - --blue: #2563eb; - --green: #16a34a; - --orange: #f97316; - --red: #dc2626; -} - -* { box-sizing: border-box; } -body { - margin: 0; - min-height: 100vh; - background: var(--bg); - color: var(--text); - font-family: Inter, Arial, sans-serif; -} - -.layout { - max-width: 1320px; - margin: 0 auto; - padding: 24px; -} - -.topbar, .section-head, .row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; -} - -.topbar { - margin-bottom: 18px; -} - -.eyebrow, .label { - margin: 0 0 6px; - color: var(--blue); - font-size: 12px; - font-weight: 700; - text-transform: uppercase; -} - -h1, h2, p { margin-top: 0; } -h1 { margin-bottom: 6px; font-size: 32px; } -h2 { margin-bottom: 4px; font-size: 18px; } -.lead, .muted, .row small, .panel p { color: var(--muted); } - -.actions { - display: flex; - gap: 10px; - align-items: center; -} - -button, a { - border: 1px solid var(--line); - background: var(--panel); - color: var(--text); - border-radius: 6px; - padding: 9px 12px; - font: inherit; - text-decoration: none; -} - -button { - background: var(--blue); - color: #fff; - cursor: pointer; -} - -.status { - margin-bottom: 16px; - border-radius: 6px; - padding: 10px 12px; - border: 1px solid var(--line); - background: #fff; -} -.status.loading { color: var(--blue); } -.status.ready { color: var(--green); } -.status.error { color: var(--red); } - -.hero-grid { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr; - gap: 14px; - margin-bottom: 14px; -} - -.work-area { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 14px; -} - -.wide { grid-column: 1 / -1; } - -.panel { - border: 1px solid var(--line); - background: var(--panel); - border-radius: 8px; - padding: 18px; - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04); -} - -.panel strong { - display: block; - font-size: 30px; - line-height: 1.15; -} - -.list { - display: grid; - gap: 10px; -} - -.row { - border-top: 1px solid var(--line); - padding-top: 10px; -} - -.row:first-child { - border-top: 0; - padding-top: 0; -} - -.badge { - border-radius: 999px; - padding: 4px 8px; - font-size: 12px; - font-weight: 700; - background: #e2e8f0; - color: #334155; -} -.badge.low, .badge.ok { background: #dcfce7; color: #166534; } -.badge.medium, .badge.warn, .badge.warning { background: #ffedd5; color: #9a3412; } -.badge.high, .badge.critical, .badge.fail { background: #fee2e2; color: #991b1b; } - -.table { - overflow-x: auto; -} - -table { - width: 100%; - border-collapse: collapse; -} - -th, td { - border-top: 1px solid var(--line); - padding: 10px 8px; - text-align: left; - vertical-align: top; -} - -th { - color: var(--muted); - font-weight: 600; -} - -@media (max-width: 980px) { - .topbar, .section-head { align-items: flex-start; flex-direction: column; } - .hero-grid, .work-area { grid-template-columns: 1fr; } - .wide { grid-column: auto; } - .layout { padding: 14px; } -} diff --git a/adk-rust/crates/detmir-dpd-portal/src/static/app.js b/adk-rust/crates/detmir-dpd-portal/src/static/app.js deleted file mode 100644 index 7c48b52..0000000 --- a/adk-rust/crates/detmir-dpd-portal/src/static/app.js +++ /dev/null @@ -1,128 +0,0 @@ -const state = { - reports: null, -}; - -const $ = (id) => document.getElementById(id); - -function setStatus(kind, text) { - const box = $("status"); - box.className = `status ${kind}`; - box.textContent = text; -} - -function text(value, fallback = "-") { - if (value === null || value === undefined || value === "") return fallback; - return String(value); -} - -function badge(level) { - const value = text(level, "UNKNOWN").toUpperCase(); - return `${value}`; -} - -function esc(value) { - return text(value, "").replace(/[&<>"']/g, (ch) => ({ - "&": "&", - "<": "<", - ">": ">", - "\"": """, - "'": "'", - }[ch])); -} - -async function loadReports() { - setStatus("loading", "Загрузка данных"); - const response = await fetch("api/reports", { cache: "no-store" }); - if (!response.ok) throw new Error(`API вернул ${response.status}`); - state.reports = await response.json(); - render(state.reports); - setStatus("ready", `Данные готовы · ${new Date().toLocaleTimeString("ru-RU")}`); -} - -function render(report) { - const summary = report.executive_dashboard?.summary || {}; - const security = report.security_events_summary || {}; - const coverage = report.agent_coverage_sla || {}; - const cases = report.executive_dashboard?.open_cases ?? 0; - $("mainRisk").textContent = text(summary.main_risk_cause, "Связанный риск не выражен"); - $("mainRiskText").textContent = text(summary.main_data_gap, "Критичных пробелов данных не найдено"); - $("securityEvents").textContent = text(security.events_24h, "0"); - $("securityStatus").textContent = security.status === "ok" - ? "События безопасности доступны" - : "Источник событий безопасности не активен"; - $("casesCount").textContent = text(cases, "0"); - $("coveragePct").textContent = coverage.coverage_pct === undefined ? "-" : `${coverage.coverage_pct}%`; - $("coverageStatus").textContent = text(coverage.sla_status, "статус не рассчитан"); - - renderBusinessRisk(report.business_risk || []); - renderCandidates(report.risk_incident_candidates || []); - renderCorrelation(report.security_correlation || []); -} - -function renderBusinessRisk(items) { - const box = $("businessRisk"); - const rows = items.slice(0, 5); - if (!rows.length) { - box.innerHTML = `

Риски подразделений не выявлены.

`; - return; - } - box.innerHTML = rows.map((item) => ` -
-
- ${esc(item.department)} - ${esc((item.reasons || []).join("; ") || "причина не указана")} -
- ${badge(item.risk_level)} -
- `).join(""); -} - -function renderCandidates(items) { - const box = $("candidates"); - const rows = items.slice(0, 5); - if (!rows.length) { - box.innerHTML = `

Кандидатов на проверку сейчас нет.

`; - return; - } - box.innerHTML = rows.map((item) => ` -
-
- ${esc(item.department || item.hostname || item.id)} - ${esc(item.reason || item.recommendation || "требует проверки")} -
- ${badge(item.risk_level)} -
- `).join(""); -} - -function renderCorrelation(items) { - const box = $("correlation"); - const rows = items.slice(0, 8); - if (!rows.length) { - box.innerHTML = `

Связь рисков и активности пока не выражена.

`; - return; - } - box.innerHTML = ` - - - - - - ${rows.map((item) => ` - - - - - - - `).join("")} - -
ПодразделениеУровень взаимосвязиПричинаРиск
${esc(item.department)}${esc(item.correlation_score ?? 0)}/100${esc(item.correlation_reason || "нет пояснения")}${badge(item.business_risk_level)}
- `; -} - -$("refreshButton").addEventListener("click", () => { - loadReports().catch((error) => setStatus("error", `Ошибка получения данных: ${error.message}`)); -}); - -loadReports().catch((error) => setStatus("error", `Ошибка получения данных: ${error.message}`)); diff --git a/adk-rust/crates/detmir-dpd-portal/src/static/index.html b/adk-rust/crates/detmir-dpd-portal/src/static/index.html deleted file mode 100644 index 7374619..0000000 --- a/adk-rust/crates/detmir-dpd-portal/src/static/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - DetMir DPD Portal - - - -
-
-
-

DetMir DPD

-

Пилотный портал

-

Параллельный read-only интерфейс для оценки будущего React/Tauri-подхода.

-
-
- - Боевой портал -
-
- -
Загрузка данных
- -
-
- Главный вывод -

Данные загружаются

-

Получение управленческого среза из текущего DetMir API.

-
-
- События безопасности - - -

Источник проверяется

-
-
- Расследования - - -

активные дела и проверки

-
-
- Полнота данных - - -

оценка покрытия агентов

-
-
- -
-
-
-

Риски подразделений

- top-5 -
-
-
- -
-
-

Требует проверки

- очередь -
-
-
- -
-
-

Связь рисков и активности

- read-only -
-
-
-
-
- - - diff --git a/adk-rust/crates/detmir-portal/src/contracts/openapi.json b/adk-rust/crates/detmir-portal/src/contracts/openapi.json index cdd2aac..9a5ad89 100644 --- a/adk-rust/crates/detmir-portal/src/contracts/openapi.json +++ b/adk-rust/crates/detmir-portal/src/contracts/openapi.json @@ -9,10 +9,6 @@ { "url": "/api", "description": "Gateway-relative API base" - }, - { - "url": "/dpd/api", - "description": "DPD mirror API base" } ], "tags": [ diff --git a/docs/ARCHITECTURE_BASELINE_RU.md b/docs/ARCHITECTURE_BASELINE_RU.md new file mode 100644 index 0000000..bc738a0 --- /dev/null +++ b/docs/ARCHITECTURE_BASELINE_RU.md @@ -0,0 +1,58 @@ +# Архитектурный baseline AWatch-rus + +Дата фиксации: 2026-06-05 + +## Целевая архитектура + +```text +Backend/API: Rust +Agent: Rust +Portal: Rust SSR + HTML + HTMX +Future Enterprise UI: React + TypeScript +Future Desktop Forensics: Tauri + React + Rust core +``` + +## Поддерживаемый контур + +- Текущий production/pilot портал: Rust server-rendered HTML с HTMX-ready + поведением и обычным JavaScript без отдельного frontend framework. +- Контрактный слой для будущих интерфейсов: JSON API, OpenAPI 3.1 и + TypeScript declarations. +- Бизнес-логика, расчёты, workflow и работа с JSON-хранилищами остаются на + стороне Rust backend/API. +- Future Enterprise UI может быть реализован на React + TypeScript только + поверх опубликованных JSON API. +- Future Desktop Forensics может быть реализован как Tauri + React + Rust core, + не ломая текущий портал. + +## Исключено из roadmap + +Dioxus и DPD Portal исключены из архитектурного roadmap проекта. + +Это означает: + +- новые Dioxus/DPD crate не добавляются; +- `/dpd/` и `/dpd/api/*` не являются частью публичного API-контракта; +- будущий UI не должен парсить HTML текущего портала; +- новые UI-фреймворки требуют отдельного architecture decision; +- breaking changes API требуют version bump контракта и migration window. + +## Compatibility baseline + +Обязательные production endpoints: + +- `/portal`; +- `/reports`; +- `/portal/reports`; +- `/api/reports`; +- `/api/contracts`; +- `/api/contracts/openapi.json`; +- `/api/contracts/typescript.d.ts`. + +Правила совместимости: + +- изменения API должны быть additive; +- клиенты игнорируют неизвестные поля; +- optional-поля могут отсутствовать; +- `null` не должен ломать UI; +- публичные JSON-поля не переименовываются без новой версии контракта. diff --git a/docs/ARCHITECTURE_CLEANUP_AUDIT_2026-06-05_RU.md b/docs/ARCHITECTURE_CLEANUP_AUDIT_2026-06-05_RU.md new file mode 100644 index 0000000..76ff064 --- /dev/null +++ b/docs/ARCHITECTURE_CLEANUP_AUDIT_2026-06-05_RU.md @@ -0,0 +1,46 @@ +# Аудит очистки архитектуры от DPD/Dioxus + +Дата: 2026-06-05 + +## Цель + +Зафиксировать результат проверки наследия DPD/Dioxus после усиления API +контрактов в коммите `bc2f56e`. + +## Найденные DPD-артефакты до очистки + +- workspace crate `adk-rust/crates/detmir-dpd-portal`; +- запись workspace member `crates/detmir-dpd-portal` в `adk-rust/Cargo.toml`; +- package entry `detmir-dpd-portal` в `adk-rust/Cargo.lock`; +- документ `docs/DPD_PORTAL_RU.md`; +- OpenAPI server base `/dpd/api`; +- упоминания DPD mirror в `docs/PORTAL_API_CONTRACTS_RU.md`; +- упоминания DPD mirror в `docs/UI_ARCHITECTURE_BASELINE_RU.md`. + +## Найденные Dioxus-артефакты до очистки + +Production-код, workspace crates, scripts, examples и OpenAPI/TypeScript +контракты не содержали Dioxus-зависимостей или `rsx!`-кода. + +Оставались только запретительные упоминания в архитектурном baseline: + +- Dioxus не является частью roadmap; +- Dioxus не добавлять без отдельного architecture decision. + +## Выполненное решение + +- DPD Portal исключён из workspace и публичного contract layer. +- Документ `docs/DPD_PORTAL_RU.md` удалён как описание экспериментальной ветки. +- OpenAPI больше не публикует `/dpd/api`. +- Runtime-слой DPD Portal выведен из эксплуатации: app-service остановлен и + выключен, gateway route удалён, перед удалением создан локальный backup. +- `docs/ARCHITECTURE_BASELINE_RU.md`, + `docs/UI_ARCHITECTURE_BASELINE_RU.md` и + `docs/PORTAL_API_CONTRACTS_RU.md` фиксируют единственную целевую архитектуру: + Rust backend/API, Rust agent, Rust SSR + HTML + HTMX portal, будущий + React/TypeScript UI и будущий Tauri + React + Rust core desktop forensics. + +## Статус + +DPD/Dioxus остаются только как исторически упомянутые исключённые направления. +Они не являются production runtime, публичным API или частью roadmap. diff --git a/docs/DPD_PORTAL_RU.md b/docs/DPD_PORTAL_RU.md deleted file mode 100644 index 0d363ba..0000000 --- a/docs/DPD_PORTAL_RU.md +++ /dev/null @@ -1,127 +0,0 @@ -# DPD Parallel Portal - -`detmir-dpd-portal` - параллельный gateway-портал DetMir. Он не заменяет -текущий `detmir-portal`, а повторяет его функциональность через отдельный -маршрут `/dpd/`. - -## Назначение - -DPD нужен для безопасной эволюции интерфейса: - -- основной HTML-портал `/portal/` остаётся стабильным; -- `/dpd/` работает как полный mirror текущего портала; -- будущий React/Tauri UI сможет использовать те же API-контракты; -- новые UI-решения проверяются без cutover и без дублирования бизнес-логики. -- будущий UI не парсит HTML текущего портала как источник данных. - -DPD проксирует: - -- вкладки; -- API; -- действия проверки; -- дела; -- расследования; -- markdown/download endpoints; -- материалы проверки. - -DPD gateway сохраняет операторский контекст для симметрии с основным порталом: -`X-Remote-User`, `X-Gateway-User`, `X-Forwarded-*`, `User-Agent`, `Referer`, -`Origin`, `Cookie` и `Authorization` передаются в upstream, hop-by-hop -заголовки не передаются. Это нужно, чтобы audit/review/case-действия через -`/dpd/` фиксировались так же, как через `/portal/`. - -## Архитектурное решение - -Выбран зрелый путь: - -```text -detmir-portal Rust backend - | - | stable /api/contracts - v -current HTML UI + DPD mirror + future React/Tauri UI -``` - -Не используется отдельный экспериментальный UI-фреймворк в production path. -Сначала фиксируются API-контракты, тестируется совместимость и только затем -добавляется новый frontend. - -## Запуск - -```bash -detmir-dpd-portal \ - --bind 127.0.0.1:8722 \ - --upstream-base http://127.0.0.1:8720 -``` - -Переменные окружения: - -- `DETMIR_DPD_BIND`; -- `DETMIR_DPD_UPSTREAM_BASE`; -- `DETMIR_DPD_TIMEOUT_SECONDS` - по умолчанию 60 секунд, чтобы первый - холодный `/api/reports` после рестарта не выглядел как отказ DPD-портала. - -## Маршруты - -- `/dpd/` - полный параллельный mirror текущего портала. -- `/dpd/_dpd/health` - health самого DPD gateway. -- `/dpd/preview/` - компактный read-only preview-экран. - -## API-контракты - -Контракты публикует основной `detmir-portal`, а DPD зеркалирует их: - -- `/api/contracts`; -- `/api/contracts/openapi.json`; -- `/api/contracts/typescript.d.ts`; -- `/dpd/api/contracts`; -- `/dpd/api/contracts/openapi.json`; -- `/dpd/api/contracts/typescript.d.ts`. - -Правила совместимости: - -- изменения API должны быть additive; -- клиенты React/Tauri обязаны игнорировать неизвестные поля; -- optional-поля могут отсутствовать; -- `null` не должен ломать UI; -- breaking changes требуют version bump контракта и отдельного architecture - decision; -- будущий React/Tauri UI не должен парсить HTML текущего портала. - -## Проверка симметрии - -Минимальная проверка на сервере: - -```bash -systemctl is-active detmir-dpd-portal detmir-portal nginx --no-pager -curl -sS http://127.0.0.1:8722/_dpd/health -curl -sS -o /dev/null -w 'dpd_index=%{http_code}\n' http://127.0.0.1:8722/ -curl -sS -o /dev/null -w 'dpd_reports=%{http_code}\n' http://127.0.0.1:8722/api/reports -curl -sS -o /dev/null -w 'dpd_contracts=%{http_code}\n' http://127.0.0.1:8722/api/contracts -``` - -Браузерная проверка с ноутбука: - -```bash -ssh -o ExitOnForwardFailure=yes -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \ - -N -L 18720:127.0.0.1:8720 - -detmir-dpd-portal \ - --bind 127.0.0.1:18722 \ - --upstream-base http://127.0.0.1:18720 - -DETMIR_PORTAL_SMOKE_URL=http://127.0.0.1:18722/ \ -DETMIR_PORTAL_SMOKE_TIMEOUT_MS=70000 \ -node scripts/detmir-portal-tabs-smoke.mjs -``` - -Ожидаемый результат: `ok=true`, все вкладки работают, нет JS/API ошибок, -абсолютные `/portal/...` ссылки в HTML/JS переписаны в `/dpd/...`. - -## Ограничения - -- DPD gateway не добавляет новые бизнес-сущности. -- DPD gateway не заменяет текущий портал. -- Данные берутся из существующего портала/API. -- Новый React/Tauri UI должен появляться поверх контрактов, а не через - копирование backend-логики. diff --git a/docs/PORTAL_API_CONTRACTS_RU.md b/docs/PORTAL_API_CONTRACTS_RU.md index d501f96..839408b 100644 --- a/docs/PORTAL_API_CONTRACTS_RU.md +++ b/docs/PORTAL_API_CONTRACTS_RU.md @@ -16,12 +16,6 @@ React/Tauri-интерфейса без переписывания backend-ло - `GET /api/contracts/openapi.json` - `GET /api/contracts/typescript.d.ts` -DPD mirror: - -- `GET /dpd/api/contracts` -- `GET /dpd/api/contracts/openapi.json` -- `GET /dpd/api/contracts/typescript.d.ts` - ## Правила совместимости - Изменения API должны быть additive. @@ -56,9 +50,9 @@ DPD mirror: - HTML-портал не удаляется. - Маршрут `/portal/` остаётся стабильным. -- DPD `/dpd/` остаётся параллельным mirror. - Backend-расчёты, JSON-хранилища и workflow не дублируются во frontend. - Публичные JSON-поля не переименовываются без новой версии контракта. +- Dioxus и DPD Portal исключены из архитектурного roadmap проекта. ## Проверка @@ -66,7 +60,6 @@ DPD mirror: curl -sS http://127.0.0.1:8720/api/contracts | jq . curl -sS http://127.0.0.1:8720/api/contracts/openapi.json | jq .openapi curl -sS http://127.0.0.1:8720/api/contracts/typescript.d.ts | head -curl -sS http://127.0.0.1:8722/api/contracts | jq . ``` Ожидаемый результат: @@ -75,4 +68,4 @@ curl -sS http://127.0.0.1:8722/api/contracts | jq . - `contract_version` заполнен; - OpenAPI JSON валиден; - TypeScript declarations доступны; -- DPD отдаёт те же контрактные endpoints через mirror. +- будущий React/Tauri UI может использовать JSON API без HTML-парсинга. diff --git a/docs/UI_ARCHITECTURE_BASELINE_RU.md b/docs/UI_ARCHITECTURE_BASELINE_RU.md index 4c9e8c4..0f8676b 100644 --- a/docs/UI_ARCHITECTURE_BASELINE_RU.md +++ b/docs/UI_ARCHITECTURE_BASELINE_RU.md @@ -10,7 +10,8 @@ Agent: Rust Current Portal: Rust server-rendered HTML + HTMX Future Enterprise UI: React + TypeScript Future Desktop Forensics: Tauri + React + Rust core -Dioxus: out of scope / не рассматривается +Dioxus: excluded from roadmap +DPD Portal: excluded from roadmap ``` ## Правила @@ -21,7 +22,7 @@ Dioxus: out of scope / не рассматривается - Будущий React/Tauri UI не должен ломать текущий портал. - Agent и backend остаются Rust-first. - Новые UI-фреймворки не добавлять без отдельного architecture decision. -- Dioxus не добавлять и не рассматривать. +- Dioxus и DPD Portal исключены из архитектурного roadmap проекта. - Будущий React/Tauri UI не должен парсить HTML текущего портала как источник данных. @@ -31,9 +32,8 @@ Dioxus: out of scope / не рассматривается 1. Укрепить текущий Rust web portal. 2. Зафиксировать стабильные JSON API-контракты. -3. Поддерживать DPD `/dpd/` как параллельный mirror для проверки совместимости. -4. Готовить будущий React/Tauri UI только поверх опубликованных контрактов. -5. Любые breaking changes проводить только через version bump контракта и +3. Готовить будущий React/Tauri UI только поверх опубликованных контрактов. +4. Любые breaking changes проводить только через version bump контракта и отдельное architecture decision. ## Контрактный слой @@ -44,12 +44,6 @@ Dioxus: out of scope / не рассматривается - `/api/contracts/openapi.json`; - `/api/contracts/typescript.d.ts`. -DPD mirror endpoints: - -- `/dpd/api/contracts`; -- `/dpd/api/contracts/openapi.json`; -- `/dpd/api/contracts/typescript.d.ts`. - Правило совместимости: изменения API должны быть additive. Клиенты обязаны игнорировать неизвестные поля, корректно обрабатывать отсутствующие optional поля и не падать на `null`.