feat: live app log console, cross-filter, JSONL export

Backend:
- /ws/app-logs — real-time app/cot log stream (raw JSONL)
- /api/logs/recent — REST snapshot of ring buffer
- GET /tasks/{id}/logs/export — streaming JSONL export with CoT parse + redaction
- Thread-safe ring buffer (seq-based polling) replaces unsafe asyncio.Queue
- GIL-friendly multi-row Core insert for log persistence
- task_id ContextVar propagates into CotJsonFormatter for CoT correlation
- Hot-apply logging level on settings update (FR-005)
- Buffer trim under DEBUG floods; drop DEBUG first, preserve ERROR/WARNING
- List projection (include_result=False) keeps reports list slim
- Security event consolidated to single REASON atom

Frontend:
- ReportsLogModel + ReportsLogPanel — full live JSONL console
- Cross-filter pinning: Tasks → Logs tab with task chip badges
- LogEntryRow — CoT-aware rendering (marker icons, expandable payload)
- TaskFilterChip, taskChipMeta — scannable type/id/env chips
- Global drawer push via CSS variable (lg+ padding, not overlay)
- i18n en/ru for all log console strings
- Ctrl+A in log panel selects only log lines (window-level handler)

QA fixes:
- Svelte 5 reactivity: SvelteDate/SvelteSet/SvelteURLSearchParams
- Fix seed_trace_id shadowing (F823) in lifecycle.py
- Remove dead code (selectedEnvironment, goToReportsPage)
- Add @BRIEF to C2 test functions, missing {#each} keys
- Remove unused import json as _json from app.py

All 3200+ frontend tests pass; backend lint clean.
This commit is contained in:
2026-07-20 21:41:34 +03:00
parent 49a566359a
commit 7bfc5553cf
44 changed files with 2808 additions and 412 deletions

View File

@@ -108,6 +108,8 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> ErrorConte
# @BRIEF Convert one Task to canonical TaskReport envelope.
# @PRE task has valid id and plugin_id fields.
# @POST Returns TaskReport with required fields and deterministic fallback behavior.
# @POST When include_result=False (list projection), omits details.result so list
# payloads stay small; detail endpoint keeps include_result=True.
#
# @TEST_CONTRACT NormalizeTaskReport ->
# {
@@ -115,14 +117,15 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> ErrorConte
# invariants: [
# "Returns a valid TaskReport object",
# "Maps TaskStatus to ReportStatus deterministically",
# "Extracts ErrorContext for FAILED/PARTIAL tasks"
# "Extracts ErrorContext for FAILED/PARTIAL tasks",
# "List projection (include_result=False) never embeds task.result"
# ]
# }
# @TEST_FIXTURE valid_task -> {"task": "MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)"}
# @TEST_EDGE task_with_error -> {"task": "MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])"}
# @TEST_EDGE unknown_plugin_type -> {"task": "MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)"}
# @TEST_INVARIANT deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]
def normalize_task_report(task: Task) -> TaskReport:
def normalize_task_report(task: Task, *, include_result: bool = True) -> TaskReport:
with belief_scope("normalize_task_report"):
task_type = resolve_task_type(task.plugin_id)
report_status = status_to_report_status(task.status)
@@ -146,6 +149,8 @@ def normalize_task_report(task: Task) -> TaskReport:
if not updated_at:
updated_at = started_at or datetime.now(UTC)
# List projection: keep only lightweight profile metadata (~150 B).
# Full task.result (LLM logs, backup dashboards, …) belongs on detail only.
details: dict[str, Any] = {
"profile": {
"display_label": profile.get("display_label"),
@@ -153,8 +158,11 @@ def normalize_task_report(task: Task) -> TaskReport:
"icon_token": profile.get("icon_token"),
"emphasis_rules": profile.get("emphasis_rules", []),
},
"result": task.result if task.result is not None else {"note": "Not provided"},
}
if include_result:
details["result"] = (
task.result if task.result is not None else {"note": "Not provided"}
)
source_ref: dict[str, Any] = {}
if isinstance(task.params, dict):