From 5f1907e374e6523fb3a0bd4a33b6a1978722549c Mon Sep 17 00:00:00 2001 From: timon0305 Date: Thu, 14 May 2026 17:28:57 +0200 Subject: [PATCH 1/3] chore: clean linter warnings + remove mypy continue-on-error (closes #29) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polish-only pass against the acceptance criteria of issue #29. No production logic changed; behaviour is identical end-to-end (178/178 unittests pass, every endpoint smokes 200 with the same response sizes as master). What landed: * mypy now exits zero on the repo. Three small annotation fixes (scripts/export.py:54 `existing: dict = {}`, api/search.py:54 `combined: list = []`, utils/exclusion_rules.py:76 `tokens: list[str | tuple[str, str]] = []`), one inner-loop variable rename in scripts/export.py to dodge a `Assignment to variable "e" outside except: block` clash with the surrounding `except … as e:`, and an empty `scripts/__init__.py` so mypy stops finding the script under two module names. * `continue-on-error: true` removed from the mypy step in `.github/workflows/tests.yml`. Type errors now fail the job. Comment updated to explain why. * Ruff + flake8 clean: 11 auto-fixed (F401 unused imports, F541 empty f-strings, F841 unused local `db_path` in api/workspaces.py), 5 manual renames of ambiguous `l` → `ln` / `log` / `layout / entry`, one F811 fix in app.py (duplicate `import sys`), one E402 import moved to the top of api/workspaces.py, and `# noqa: E402` markers on the scripts/export.py imports that legitimately come after `sys.path.insert(0, …)`. * Bare `print()` error logging in api/composers.py (2), api/search.py (3) and api/workspaces.py (7) replaced with `_logger = logging.getLogger(__name__)` calls. Endpoint-level catches use `_logger.exception(...)` so the traceback survives; the two per-row catches (`CLI: could not read session …` and `Error parsing composer data for …`) use `_logger.warning(..., exc_info=True)` because they're per-iteration, not endpoint-level. Matches the convention already used in scripts/export.py:42 and utils/exclusion_rules.py:191. Verified locally: - python3 -m mypy --ignore-missing-imports --no-strict-optional . → Success: no issues found in 35 source files - ruff check api/ utils/ scripts/export.py app.py → All checks passed - flake8 --select=F,E9,W6 api/ utils/ scripts/export.py app.py → clean - python3 -m unittest discover tests → Ran 178 tests, OK - python3 app.py boots clean; GET / + /api/workspaces + /api/composers + /api/search?q=foo all return 200 --- .github/workflows/tests.yml | 5 ++-- api/composers.py | 10 ++++--- api/export_api.py | 11 ++++---- api/logs.py | 2 +- api/search.py | 20 ++++++++------ api/workspaces.py | 37 +++++++++++++------------ app.py | 1 - scripts/__init__.py | 0 scripts/export.py | 55 +++++++++++++++++++------------------ utils/cursor_md_exporter.py | 1 - utils/exclusion_rules.py | 2 +- 11 files changed, 73 insertions(+), 71 deletions(-) create mode 100644 scripts/__init__.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3ee7797..a597753 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -75,9 +75,8 @@ jobs: python -m pip install 'flask>=3.0' 'fpdf2>=2.7' 'mypy>=1.10' - name: Run mypy - # Transitional only (maintainer consensus): keeps CI green until `mypy` exits - # zero on this repo — then delete this line so type errors fail the job. - continue-on-error: true + # No `continue-on-error` — mypy now exits zero on this repo (closes #29), + # so type errors must fail the job from here on. run: mypy --ignore-missing-imports --no-strict-optional --pretty . # ── Secret scan: gitleaks ───────────────────────────────────────────────── diff --git a/api/composers.py b/api/composers.py index 9f8ef2a..8740356 100644 --- a/api/composers.py +++ b/api/composers.py @@ -5,6 +5,7 @@ """ import json +import logging import os import sqlite3 from contextlib import closing @@ -15,6 +16,7 @@ from utils.path_helpers import to_epoch_ms bp = Blueprint("composers", __name__) +_logger = logging.getLogger(__name__) def _read_json_file(path: str): @@ -67,8 +69,8 @@ def list_composers(): composers.sort(key=lambda c: to_epoch_ms(c.get("lastUpdatedAt")), reverse=True) return jsonify(composers) - except Exception as e: - print(f"Failed to get composers: {e}") + except Exception: + _logger.exception("Failed to get composers") return jsonify({"error": "Failed to get composers"}), 500 @@ -122,6 +124,6 @@ def get_composer(composer_id): return jsonify({"error": "Composer not found"}), 404 - except Exception as e: - print(f"Failed to get composer: {e}") + except Exception: + _logger.exception("Failed to get composer") return jsonify({"error": "Failed to get composer"}), 500 diff --git a/api/export_api.py b/api/export_api.py index 3144925..2cff8f9 100644 --- a/api/export_api.py +++ b/api/export_api.py @@ -9,7 +9,6 @@ import os import re import sqlite3 -import sys import zipfile from contextlib import closing from datetime import datetime @@ -18,7 +17,7 @@ from flask import Blueprint, Response, current_app, jsonify, request from utils.workspace_path import resolve_workspace_path -from utils.path_helpers import normalize_file_path, get_workspace_folder_paths, to_epoch_ms +from utils.path_helpers import get_workspace_folder_paths, to_epoch_ms from utils.text_extract import extract_text_from_bubble from utils.tool_parser import parse_tool_call from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules @@ -390,15 +389,15 @@ def export_chats(): status_str = f" ({tool_status})" if tool_status else "" md += f"> **Tool: {tool_summary}**{status_str}\n" if t.get("input"): - md += f">\n> **INPUT:**\n> ```\n" + md += ">\n> **INPUT:**\n> ```\n" for iline in str(t["input"]).split("\n"): md += f"> {iline}\n" - md += f"> ```\n" + md += "> ```\n" if t.get("output"): - md += f">\n> **OUTPUT:**\n> ```\n" + md += ">\n> **OUTPUT:**\n> ```\n" for oline in str(t["output"]).split("\n"): md += f"> {oline}\n" - md += f"> ```\n" + md += "> ```\n" md += "\n" md += "---\n\n" diff --git a/api/logs.py b/api/logs.py index a512c1c..e361b41 100644 --- a/api/logs.py +++ b/api/logs.py @@ -133,7 +133,7 @@ def get_logs(): except Exception: pass - logs.sort(key=lambda l: l.get("timestamp") or 0, reverse=True) + logs.sort(key=lambda log: log.get("timestamp") or 0, reverse=True) return jsonify({"logs": logs}) except Exception as e: diff --git a/api/search.py b/api/search.py index f08a1ae..be55885 100644 --- a/api/search.py +++ b/api/search.py @@ -4,6 +4,7 @@ """ import json +import logging import os import re import sqlite3 @@ -15,11 +16,12 @@ from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules from utils.workspace_path import resolve_workspace_path, get_cli_chats_path -from utils.path_helpers import normalize_file_path, get_workspace_folder_paths, to_epoch_ms +from utils.path_helpers import to_epoch_ms from utils.text_extract import extract_text_from_bubble from utils.cli_chat_reader import list_cli_projects, traverse_blobs, messages_to_bubbles bp = Blueprint("search", __name__) +_logger = logging.getLogger(__name__) def _json_dump_safe(value) -> str: @@ -51,7 +53,7 @@ def _build_exclusion_searchable( metadata_parts: list[str] | None = None, ) -> str: """Build broad searchable text so exclusion rules cover visible output.""" - combined = [] + combined: list = [] if content_parts: combined.extend(p for p in content_parts if p) if metadata_parts: @@ -231,7 +233,7 @@ def search(): # Derive title from first bubble for text in bubble_texts: if text: - first_lines = [l for l in text.split("\n") if l.strip()] + first_lines = [ln for ln in text.split("\n") if ln.strip()] if first_lines: title = first_lines[0][:100] break @@ -250,8 +252,8 @@ def search(): except Exception: pass - except Exception as e: - print(f"Error searching global storage: {e}") + except Exception: + _logger.exception("Error searching global storage") finally: if conn is not None: conn.close() @@ -435,8 +437,8 @@ def search(): "type": "cli_agent", "source": "cli", }) - except Exception as e: - print(f"Error searching CLI sessions: {e}") + except Exception: + _logger.exception("Error searching CLI sessions") # Sort by timestamp descending def _ts(r): @@ -451,6 +453,6 @@ def _ts(r): return jsonify({"results": results}) - except Exception as e: - print(f"Search failed: {e}") + except Exception: + _logger.exception("Search failed") return jsonify({"error": "Search failed", "results": []}), 500 diff --git a/api/workspaces.py b/api/workspaces.py index ad11be4..b179140 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -8,6 +8,7 @@ from __future__ import annotations import json +import logging import os import re import sqlite3 @@ -32,9 +33,11 @@ to_epoch_ms, ) from utils.text_extract import extract_text_from_bubble, format_tool_action +from utils.tool_parser import parse_tool_call as _parse_tool_call from utils.exclusion_rules import build_searchable_text, is_excluded_by_rules bp = Blueprint("workspaces", __name__) +_logger = logging.getLogger(__name__) def _get_workspace_display_name(workspace_path: str, workspace_id: str) -> str: @@ -663,7 +666,6 @@ def list_workspaces(): primary = group[0] all_ws_ids = [e["name"] for e in group] - db_path = os.path.join(workspace_path, primary["name"], "state.vscdb") try: mtime = max( os.path.getmtime(os.path.join(workspace_path, e["name"], "state.vscdb")) @@ -758,14 +760,14 @@ def list_workspaces(): ), "source": "cli", }) - except Exception as e: - print(f"Failed to load CLI projects: {e}") + except Exception: + _logger.exception("Failed to load CLI projects") projects.sort(key=lambda p: p["lastModified"], reverse=True) return jsonify(projects) - except Exception as e: - print(f"Failed to get workspaces: {e}") + except Exception: + _logger.exception("Failed to get workspaces") return jsonify({"error": "Failed to get workspaces"}), 500 @@ -839,8 +841,8 @@ def get_workspace(workspace_id): "lastModified": datetime.fromtimestamp(mtime, tz=timezone.utc).isoformat(), }) - except Exception as e: - print(f"Failed to get workspace: {e}") + except Exception: + _logger.exception("Failed to get workspace") return jsonify({"error": "Failed to get workspace"}), 500 @@ -848,7 +850,6 @@ def get_workspace(workspace_id): # GET /api/workspaces//tabs # --------------------------------------------------------------------------- -from utils.tool_parser import parse_tool_call as _parse_tool_call def _get_cli_workspace_tabs(workspace_id: str): @@ -872,8 +873,8 @@ def _get_cli_workspace_tabs(workspace_id: str): try: messages = traverse_blobs(session["db_path"]) - except Exception as e: - print(f"CLI: could not read session {session_id}: {e}") + except Exception: + _logger.warning("CLI: could not read session %s", session_id, exc_info=True) continue bubbles = messages_to_bubbles(messages, created_ms) @@ -885,7 +886,7 @@ def _get_cli_workspace_tabs(workspace_id: str): if not title or title.startswith("New Agent"): for b in bubbles: if b["type"] == "user" and b.get("text"): - first_lines = [l for l in b["text"].split("\n") if l.strip()] + first_lines = [ln for ln in b["text"].split("\n") if ln.strip()] if first_lines: title = first_lines[0][:100] if len(title) == 100: @@ -937,8 +938,8 @@ def _get_cli_workspace_tabs(workspace_id: str): tabs.sort(key=lambda t: t.get("timestamp") or 0, reverse=True) return jsonify({"tabs": tabs}) - except Exception as e: - print(f"Failed to get CLI workspace tabs: {e}") + except Exception: + _logger.exception("Failed to get CLI workspace tabs") return jsonify({"error": "Failed to get CLI workspace tabs"}), 500 @@ -1251,7 +1252,7 @@ def get_workspace_tabs(workspace_id): if not cd.get("name") and bubbles: first_msg = bubbles[0].get("text", "") if first_msg: - first_lines = [l for l in first_msg.split("\n") if l.strip()] + first_lines = [ln for ln in first_msg.split("\n") if ln.strip()] if first_lines: title = first_lines[0][:100] if len(title) == 100: @@ -1394,14 +1395,14 @@ def get_workspace_tabs(workspace_id): response["tabs"].append(tab) - except Exception as e: - print(f"Error parsing composer data for {composer_id}: {e}") + except Exception: + _logger.warning("Error parsing composer data for %s", composer_id, exc_info=True) # Sort tabs by timestamp descending (newest first) response["tabs"].sort(key=lambda t: t.get("timestamp") or 0, reverse=True) return jsonify(response) - except Exception as e: - print(f"Failed to get workspace tabs: {e}") + except Exception: + _logger.exception("Failed to get workspace tabs") return jsonify({"error": "Failed to get workspace tabs"}), 500 diff --git a/app.py b/app.py index f04feae..1caa215 100644 --- a/app.py +++ b/app.py @@ -90,7 +90,6 @@ def favicon(): if __name__ == "__main__": import argparse - import sys parser = argparse.ArgumentParser(description="Cursor Chat Browser (Python)") parser.add_argument("--port", type=int, default=3000) diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/export.py b/scripts/export.py index d79fb66..a879b91 100644 --- a/scripts/export.py +++ b/scripts/export.py @@ -22,22 +22,23 @@ if str(_project_root) not in sys.path: sys.path.insert(0, str(_project_root)) -from utils.exclusion_rules import ( +# noqa: E402 — these imports must come after the sys.path.insert above so the +# script can be run directly as `python scripts/export.py` from anywhere. +from utils.exclusion_rules import ( # noqa: E402 resolve_exclusion_rules_path, load_rules, build_searchable_text, is_excluded_by_rules, ) -from utils.path_helpers import get_workspace_folder_paths as _shared_get_workspace_folder_paths -from utils.tool_parser import parse_tool_call -from utils.workspace_path import get_cli_chats_path -from utils.cli_chat_reader import ( +from utils.path_helpers import get_workspace_folder_paths as _shared_get_workspace_folder_paths # noqa: E402 +from utils.tool_parser import parse_tool_call # noqa: E402 +from utils.workspace_path import get_cli_chats_path # noqa: E402 +from utils.cli_chat_reader import ( # noqa: E402 list_cli_projects, traverse_blobs, messages_to_bubbles, - aggregate_session_stats, ) -from utils.cursor_md_exporter import cursor_cli_session_to_markdown +from utils.cursor_md_exporter import cursor_cli_session_to_markdown # noqa: E402 _logger = logging.getLogger(__name__) @@ -52,7 +53,7 @@ def _json_dump_safe(value) -> str: def _load_manifest_entries(manifest_path: str) -> dict: """Load manifest entries keyed by log_id from a JSONL file.""" - existing = {} + existing: dict = {} if not os.path.isfile(manifest_path): return existing try: @@ -347,9 +348,9 @@ def main(): layouts = ctx.get("projectLayouts") if isinstance(layouts, list): project_layouts_map.setdefault(cid, []) - for l in layouts: + for layout in layouts: try: - o = json.loads(l) if isinstance(l, str) else l + o = json.loads(layout) if isinstance(layout, str) else layout if isinstance(o, dict) and o.get("rootPath"): project_layouts_map[cid].append(o["rootPath"]) except Exception: @@ -682,7 +683,7 @@ def assign_workspace(cd, cid): created_ms = to_epoch_ms(cd.get("createdAt")) or ts fm_lines = ["---"] fm_lines.append(f"log_id: {composer_id}") - fm_lines.append(f"log_type: chat") + fm_lines.append("log_type: chat") fm_lines.append(f'title: "{title.replace(chr(34), chr(92)+chr(34))}"') fm_lines.append(f"created_at: {datetime.fromtimestamp(created_ms / 1000).isoformat()}") fm_lines.append(f"updated_at: {datetime.fromtimestamp(updated_at / 1000).isoformat() if updated_at else datetime.now().isoformat()}") @@ -930,25 +931,25 @@ def assign_workspace(cd, cid): zip_name = f"cursor-export-{today}.zip" zip_path = os.path.join(out_dir, zip_name) with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: - for e in exported: - zf.writestr(e["rel_path"], e["content"]) + for entry in exported: + zf.writestr(entry["rel_path"], entry["content"]) print(f"Exported {count} chat(s) to {zip_path}") else: # Write individual Markdown files to disk - for e in exported: - os.makedirs(os.path.dirname(e["out_path"]), exist_ok=True) - with open(e["out_path"], "w", encoding="utf-8") as f: - f.write(e["content"]) + for entry in exported: + os.makedirs(os.path.dirname(entry["out_path"]), exist_ok=True) + with open(entry["out_path"], "w", encoding="utf-8") as f: + f.write(entry["content"]) # Manifest in output directory manifest_path = os.path.join(out_dir, "manifest.jsonl") existing = _load_manifest_entries(manifest_path) - for e in exported: - existing[e["id"]] = { - "log_id": e["id"], - "path": os.path.relpath(e["out_path"], out_dir), - "updated_at": datetime.fromtimestamp(e["updatedAt"] / 1000).isoformat() if e["updatedAt"] else datetime.now().isoformat(), + for entry in exported: + existing[entry["id"]] = { + "log_id": entry["id"], + "path": os.path.relpath(entry["out_path"], out_dir), + "updated_at": datetime.fromtimestamp(entry["updatedAt"] / 1000).isoformat() if entry["updatedAt"] else datetime.now().isoformat(), } if existing: @@ -957,11 +958,11 @@ def assign_workspace(cd, cid): # Canonical manifest in user state dir so tracking survives changing --out paths global_manifest_path = os.path.join(state_dir, "manifest.jsonl") global_existing = _load_manifest_entries(global_manifest_path) - for e in exported: - global_existing[e["id"]] = { - "log_id": e["id"], - "path": e["out_path"], - "updated_at": datetime.fromtimestamp(e["updatedAt"] / 1000).isoformat() if e["updatedAt"] else datetime.now().isoformat(), + for entry in exported: + global_existing[entry["id"]] = { + "log_id": entry["id"], + "path": entry["out_path"], + "updated_at": datetime.fromtimestamp(entry["updatedAt"] / 1000).isoformat() if entry["updatedAt"] else datetime.now().isoformat(), } if global_existing: _write_manifest_entries(global_manifest_path, global_existing) diff --git a/utils/cursor_md_exporter.py b/utils/cursor_md_exporter.py index 2a08158..8ec9d91 100644 --- a/utils/cursor_md_exporter.py +++ b/utils/cursor_md_exporter.py @@ -11,7 +11,6 @@ import json from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING from utils.cli_chat_reader import traverse_blobs, messages_to_bubbles diff --git a/utils/exclusion_rules.py b/utils/exclusion_rules.py index 2fe8531..18bc54d 100644 --- a/utils/exclusion_rules.py +++ b/utils/exclusion_rules.py @@ -73,7 +73,7 @@ def _tokenize_rule(line: str) -> list: the string ``"OR"``, or a ``(kind, value)`` tuple where *kind* is ``"word"`` or ``"phrase"``. """ - tokens = [] + tokens: list[str | tuple[str, str]] = [] rest = line.strip() while rest: # Skip whitespace From 7bbfe43d626d50b76beecdc75838000ba5772e79 Mon Sep 17 00:00:00 2001 From: timon0305 Date: Thu, 14 May 2026 17:36:41 +0200 Subject: [PATCH 2/3] review: silence BLE001 on the two CodeRabbit-cited best-effort catches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api/workspaces.py:876 (per-CLI-session traverse_blobs catch) and api/workspaces.py:1398 (per-composer-row tabs parse catch) are intentionally broad — a single malformed session or composer must not 500 the whole endpoint, and `exc_info=True` on the .warning() call already captures the concrete exception type for debugging. Added `# noqa: BLE001` with a one-line rationale on each line so the intent survives future readers. CodeRabbit's review on PR #35 cited exactly these two sites; the other BLE001 hits in the file are pre-existing and out of scope for issue #29 ("polish only, no logic changes"). Verified: full suite still 178/178; ruff default rule set still clean. --- api/workspaces.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/workspaces.py b/api/workspaces.py index b179140..dde6a63 100644 --- a/api/workspaces.py +++ b/api/workspaces.py @@ -873,7 +873,7 @@ def _get_cli_workspace_tabs(workspace_id: str): try: messages = traverse_blobs(session["db_path"]) - except Exception: + except Exception: # noqa: BLE001 — best-effort per-session skip; one corrupted session must not 500 the endpoint, and the failure mode is logged with exc_info so the concrete type is preserved. _logger.warning("CLI: could not read session %s", session_id, exc_info=True) continue @@ -1395,7 +1395,7 @@ def get_workspace_tabs(workspace_id): response["tabs"].append(tab) - except Exception: + except Exception: # noqa: BLE001 — best-effort per-composer skip in a read-many loop; one malformed row must not 500 the tabs endpoint, and exc_info captures the concrete type for debugging. _logger.warning("Error parsing composer data for %s", composer_id, exc_info=True) # Sort tabs by timestamp descending (newest first) From 59232145c716e8251c1728737e8f999459968d24 Mon Sep 17 00:00:00 2001 From: timon0305 Date: Thu, 14 May 2026 22:31:21 +0200 Subject: [PATCH 3/3] review: address 3 Brad findings on PR #35 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `.github/workflows/tests.yml` — block comment above the typecheck job still described `continue-on-error: true` as the transitional policy, contradicting the actual code which removed it in this same PR. Updated the comment to reflect the enforced gate. * `api/logs.py` — out of issue #29's literal scope (issue lists only the three files I converted in the previous commit), but inconsistent with the "clean demo" spirit. Two `print()` error logs at lines 73 and 140 swapped for `_logger.exception(...)` matching the pattern in api/workspaces.py / api/search.py / api/composers.py. * `api/search.py:56` — `combined: list = []` is the minimum annotation that satisfies mypy, but `list[str]` is the actual shape: both content_parts and metadata_parts are typed `list[str]` upstream, the filter `(p for p in ... if p)` only adds truthy strings, and the consumer is `"\n\n".join(combined)` which requires strings. Tightened. Verified locally: - mypy --ignore-missing-imports --no-strict-optional . → Success - ruff check api/ utils/ scripts/export.py app.py → All checks passed - unittest discover tests → 178 / 178 OK --- .github/workflows/tests.yml | 6 +++--- api/logs.py | 10 ++++++---- api/search.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a597753..d12ffb5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,9 +55,9 @@ jobs: # Codebase already has type hints across most of the surface (~70+ typed # functions). Mypy runs in lenient mode (--ignore-missing-imports for # untyped third-party deps; no strict-optional) so the gate isn't a wall - # of false positives on first run. continue-on-error keeps findings as - # warnings during the surface-cleanup phase; flip to required by removing - # continue-on-error once the surface is clean. + # of false positives. The transitional `continue-on-error: true` was + # removed in #29 once mypy reached zero errors on this repo — type + # failures now block merges. typecheck: name: Typecheck (mypy) runs-on: ubuntu-latest diff --git a/api/logs.py b/api/logs.py index e361b41..a213cdc 100644 --- a/api/logs.py +++ b/api/logs.py @@ -4,6 +4,7 @@ """ import json +import logging import os import re import sqlite3 @@ -16,6 +17,7 @@ from utils.path_helpers import to_epoch_ms bp = Blueprint("logs", __name__) +_logger = logging.getLogger(__name__) def _extract_chat_id_from_bubble_key(key: str) -> str | None: @@ -69,8 +71,8 @@ def get_logs(): "type": "chat", "messageCount": len(bubbles), }) - except Exception as e: - print(f"Error reading global storage: {e}") + except Exception: + _logger.exception("Error reading global storage") # Per-workspace (legacy) try: @@ -136,6 +138,6 @@ def get_logs(): logs.sort(key=lambda log: log.get("timestamp") or 0, reverse=True) return jsonify({"logs": logs}) - except Exception as e: - print(f"Failed to get logs: {e}") + except Exception: + _logger.exception("Failed to get logs") return jsonify({"error": "Failed to get logs", "logs": []}), 500 diff --git a/api/search.py b/api/search.py index be55885..fe2b6eb 100644 --- a/api/search.py +++ b/api/search.py @@ -53,7 +53,7 @@ def _build_exclusion_searchable( metadata_parts: list[str] | None = None, ) -> str: """Build broad searchable text so exclusion rules cover visible output.""" - combined: list = [] + combined: list[str] = [] if content_parts: combined.extend(p for p in content_parts if p) if metadata_parts: