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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user