E4 (B21): отправка зон в КОНТУР по операции (контракт B20)
- contour_operation_id в PATCH /operations/{id} (mapping на операцию КОНТУРа).
- backend/routers/contour.py: POST /{id}/send-to-contour — последний анализ
(search_models → legacy analysis_log) → SearchZones[] GeoJSON-сектора
направлений (±22.5°, probability по приоритету) → POST {CONTOUR_API_URL}/
api/v1/vector/zones?operation_id=... (Bearer CONTOUR_TOKEN). Все шаги
аудируются (contour_send_ok/failed). GET /{id}/areas-checked — прокси
проверенных квадратов (inbound к B17).
- env: CONTOUR_API_URL (дефолт CT130:8000), CONTOUR_TOKEN (в secrets).
- case_id-фильтр в GET /operations.
- UI: панель «КОНТУР» на странице анализа: ввод/сохранение UUID операции
КОНТУРа, кнопка «Отправить зоны в КОНТУР» со статусом результата.
- Тесты E4 (4): build B20-payload (полигон замкнут, probability 0..1,
operation_id в query, Bearer), недостающий contour_id 400, отказ КОНТУРа
→ 502 + аудит, геометрия сектора.
226 passed, 5 skipped.
This commit is contained in:
@@ -679,4 +679,37 @@
|
||||
.case-info { flex-direction: column; align-items: flex-start; gap: 6px; }
|
||||
.separator { display: none; }
|
||||
.tactics-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
}
|
||||
/* E4: панель КОНТУР */
|
||||
.contour-panel {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #161b22;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.contour-title { font-size: 13px; font-weight: 600; color: #e6edf3; margin-bottom: 8px; }
|
||||
.contour-row { display: flex; gap: 8px; margin-bottom: 8px; }
|
||||
.contour-input {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
background: #0d1117;
|
||||
border: 1px solid #30363d;
|
||||
border-radius: 6px;
|
||||
color: #e6edf3;
|
||||
font-size: 13px;
|
||||
}
|
||||
.contour-save, .contour-send {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.contour-save { background: #21262d; color: #e6edf3; }
|
||||
.contour-send { background: #238636; }
|
||||
.contour-send:disabled { background: #21262d; color: #8b949e; cursor: not-allowed; }
|
||||
.contour-ok { font-size: 12px; color: #3fb950; }
|
||||
.contour-err { font-size: 12px; color: #f85149; }
|
||||
|
||||
@@ -36,6 +36,9 @@ const AnalysisResult = () => {
|
||||
const [caseData, setCaseData] = useState(null);
|
||||
const [analysisData, setAnalysisData] = useState(null);
|
||||
const [waterData, setWaterData] = useState(null);
|
||||
const [operation, setOperation] = useState(null);
|
||||
const [contourId, setContourId] = useState('');
|
||||
const [contourStatus, setContourStatus] = useState(null); // {ok, message}
|
||||
|
||||
useEffect(() => {
|
||||
const loadCaseAndAnalyze = async () => {
|
||||
@@ -49,6 +52,18 @@ const AnalysisResult = () => {
|
||||
const caseResult = await caseResponse.json();
|
||||
setCaseData(caseResult);
|
||||
|
||||
// E4: операция этого кейса (для КОНТУР-отправки)
|
||||
try {
|
||||
const opsResp = await apiFetch(`/api/v1/operations?case_id=${caseId}`);
|
||||
if (opsResp.ok) {
|
||||
const opsData = await opsResp.json();
|
||||
if (opsData.items?.length) {
|
||||
setOperation(opsData.items[0]);
|
||||
setContourId(opsData.items[0].contour_operation_id || '');
|
||||
}
|
||||
}
|
||||
} catch { /* операции может не быть — панель скрыта */ }
|
||||
|
||||
// 2. Анализ — единый источник правды на бэкенде
|
||||
const analyzeResponse = await apiFetch(`/api/v1/analyze`, {
|
||||
method: 'POST',
|
||||
@@ -228,6 +243,49 @@ const AnalysisResult = () => {
|
||||
) : (
|
||||
<div className="map-empty">ТНП не задана — карта недоступна</div>
|
||||
)}
|
||||
{operation && (
|
||||
<div className="contour-panel">
|
||||
<div className="contour-title">🔗 КОНТУР (полевая координация)</div>
|
||||
<div className="contour-row">
|
||||
<input
|
||||
className="contour-input"
|
||||
placeholder="UUID операции в КОНТУРе (создаётся в штабной панели)"
|
||||
value={contourId}
|
||||
onChange={(e) => setContourId(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="contour-save"
|
||||
onClick={async () => {
|
||||
const r = await apiFetch(`/api/v1/operations/${operation.id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ contour_operation_id: contourId }),
|
||||
});
|
||||
setContourStatus(r.ok
|
||||
? { ok: true, message: 'ID операции КОНТУРа сохранён' }
|
||||
: { ok: false, message: 'Не удалось сохранить ID' });
|
||||
}}
|
||||
>Сохранить ID</button>
|
||||
</div>
|
||||
<div className="contour-row">
|
||||
<button
|
||||
className="contour-send"
|
||||
disabled={!contourId || !analysisData}
|
||||
onClick={async () => {
|
||||
const r = await apiFetch(`/api/v1/operations/${operation.id}/send-to-contour`, { method: 'POST' });
|
||||
const body = await r.json().catch(() => ({}));
|
||||
setContourStatus(r.ok
|
||||
? { ok: true, message: `Отправлено зон: ${body.sent_zones}. Статус в КОНТУРе: ${body.contour_response?.status}` }
|
||||
: { ok: false, message: body.detail || 'Ошибка отправки' });
|
||||
}}
|
||||
>Отправить зоны в КОНТУР</button>
|
||||
</div>
|
||||
{contourStatus && (
|
||||
<div className={contourStatus.ok ? 'contour-ok' : 'contour-err'}>
|
||||
{contourStatus.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="map-info">
|
||||
<span>ТНП: {tnpLat ? Number(tnpLat).toFixed(5) : '—'}, {tnpLon ? Number(tnpLon).toFixed(5) : '—'}</span>
|
||||
{caseData.tnp_address && <span className="sep">·</span>}
|
||||
|
||||
Reference in New Issue
Block a user