From c033a211d98774538de1bc4a4ae49dc48ba7a9ad Mon Sep 17 00:00:00 2001 From: Laurent Grawet Date: Sun, 30 Aug 2026 14:25:12 +0200 Subject: [PATCH 1/4] (fix) render markdown links in routine results across HTML, web and Telegram Report table cells and routine-result tables now turn a full [label](url) cell into a clickable link (escaped otherwise), the report iframe sandbox gains allow-popups-to-escape-sandbox so links open in a new tab, and Telegram routine results keep links/bold rendered instead of escaping them inside a code block. Co-Authored-By: Claude Opus 4.7 --- condor/reports/builder.py | 25 ++++++++++++- .../src/components/routines/ReportFrame.tsx | 2 +- .../components/routines/RoutineResultView.tsx | 26 +++++++++++++- handlers/routines/__init__.py | 14 +++++--- utils/telegram_formatters.py | 35 +++++++++++++++++++ 5 files changed, 94 insertions(+), 8 deletions(-) diff --git a/condor/reports/builder.py b/condor/reports/builder.py index a9fd47837..734cbbdd5 100644 --- a/condor/reports/builder.py +++ b/condor/reports/builder.py @@ -35,6 +35,29 @@ } +_CELL_LINK_RE = re.compile( + r"^\[([^\]\n]+)\]\((https?://[^)\s]+)\)(\s*\S*)?$" +) + + +def _escape_cell(value: object) -> str: + """Escape a table cell; a full ``[label](url)`` becomes a clickable link. + + Only a cell whose whole value is a markdown link (an optional trailing + marker such as `` ⚠`` is allowed) is converted to an ````. Everything + else is escaped verbatim, so existing plain-text cells render unchanged. + """ + text = str(value) + m = _CELL_LINK_RE.match(text) + if m: + return ( + f'' + f"{html.escape(m.group(1))}{html.escape(m.group(3) or '')}" + ) + return html.escape(text) + + class ReportBuilder: """Compose a report section by section, then ``save()`` it. @@ -561,7 +584,7 @@ def _render_table(columns: list[str], rows: list[dict]) -> str: body_rows = [] for row in rows: cells = "".join( - f"{html.escape(str(row.get(column, '')))}" + f"{_escape_cell(row.get(column, ''))}" for column in columns ) body_rows.append(f"{cells}") diff --git a/frontend/src/components/routines/ReportFrame.tsx b/frontend/src/components/routines/ReportFrame.tsx index f6f1f1439..4ca29dbe1 100644 --- a/frontend/src/components/routines/ReportFrame.tsx +++ b/frontend/src/components/routines/ReportFrame.tsx @@ -77,7 +77,7 @@ export function ReportFrame({ srcDoc={html} className={`h-full w-full border-0 ${className}`} title={title} - sandbox="allow-scripts allow-popups allow-downloads" + sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-downloads" /> ); } diff --git a/frontend/src/components/routines/RoutineResultView.tsx b/frontend/src/components/routines/RoutineResultView.tsx index b79e3601b..c5749b480 100644 --- a/frontend/src/components/routines/RoutineResultView.tsx +++ b/frontend/src/components/routines/RoutineResultView.tsx @@ -7,6 +7,30 @@ interface Props { instance: RoutineInstance; } +const CELL_LINK_RE = /^\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)(\s*\S*)?$/; + +function formatCellValue(val: unknown) { + if (typeof val === "number") { + // Numbers keep their exact formatting — a link can be a numeric-looking + // string while only strings matching the full [label](url) pattern change. + return val.toFixed(val % 1 === 0 ? 0 : 2); + } + const s = String(val ?? ""); + const m = CELL_LINK_RE.exec(s); + if (m) { + // Same [label](url) convention as report table cells (ReportBuilder). + return ( + <> + + {m[1]} + + {m[3] ?? ""} + + ); + } + return s; +} + interface KpiSection { type: "kpi"; label: string; @@ -141,7 +165,7 @@ export function RoutineResultView({ instance }: Props) { : ""; return ( - {isNum ? (val as number).toFixed(val % 1 === 0 ? 0 : 2) : String(val ?? "")} + {formatCellValue(val)} ); })} diff --git a/handlers/routines/__init__.py b/handlers/routines/__init__.py index 6355064db..c5636b35a 100644 --- a/handlers/routines/__init__.py +++ b/handlers/routines/__init__.py @@ -30,7 +30,11 @@ normalize_result, ) from utils.auth import restricted -from utils.telegram_formatters import escape_markdown_v2, escape_markdown_v2_code +from utils.telegram_formatters import ( + escape_markdown_v2, + escape_markdown_v2_code, + format_routine_result, +) logger = logging.getLogger(__name__) @@ -547,7 +551,7 @@ async def _interval_job_callback(context: CallbackContext) -> None: text = ( f"{icon} *{escape_markdown_v2(_display_name(routine_name))}* `{instance_id}`\n" f"⏱️ {escape_markdown_v2(interval_str)} \\| Run \\#{run_count} \\| {escape_markdown_v2(_format_duration(duration))}\n\n" - f"```\n{escape_markdown_v2_code(result[:400])}\n```" + f"{format_routine_result(result)}" ) try: keyboard = None @@ -614,7 +618,7 @@ async def _oneshot_job_callback(context: CallbackContext) -> None: text = ( f"{icon} *{escape_markdown_v2(_display_name(routine_name))}*\n" f"Duration: {escape_markdown_v2(_format_duration(duration))}\n\n" - f"```\n{escape_markdown_v2_code(result[:400])}\n```" + f"{format_routine_result(result)}" ) try: keyboard = None @@ -688,7 +692,7 @@ async def _daily_job_callback(context: CallbackContext) -> None: icon = "✅" if not result.startswith("Error") else "❌" text = ( f"{icon} *Daily: {escape_markdown_v2(_display_name(routine_name))}*\n" - f"```\n{escape_markdown_v2_code(result[:400])}\n```" + f"{format_routine_result(result)}" ) try: keyboard = None @@ -1340,7 +1344,7 @@ async def _refresh_detail_msg( dur_str = _format_duration(duration) if duration else "" result_section = ( f"\n\n┌─ {icon} Result ─ {escape_markdown_v2(dur_str)} ────\n" - f"```\n{escape_markdown_v2_code(result[:250])}\n```\n" + f"{format_routine_result(result, max_len=250)}\n" f"└────────────────────────────" ) diff --git a/utils/telegram_formatters.py b/utils/telegram_formatters.py index 171549788..dc5b4c043 100644 --- a/utils/telegram_formatters.py +++ b/utils/telegram_formatters.py @@ -135,6 +135,41 @@ def escape_markdown_v2_code(text: str) -> str: return str(text).replace("\\", "\\\\").replace("`", "\\`") +def format_routine_result(result: str, max_len: int = 400) -> str: + """Format a routine result for a Telegram MarkdownV2 message. + + Results containing ``[label](url)`` links are rendered outside a code + block (which would suppress the links), keeping the links clickable and + ``**bold**`` markers rendered, with everything else escaped. Results + without links keep the monospace code block, so existing routines render + exactly as before. + """ + result = str(result)[:max_len] + if not re.search(r"\[[^\]\n]+\]\(https?://[^)\s]+\)", result): + return f"```\n{escape_markdown_v2_code(result)}\n```" + return _markdown_v2_with_links(result) + + +def _markdown_v2_with_links(text: str) -> str: + """Escape text for MarkdownV2 but keep [label](url) links and **bold** + markers rendered instead of escaped.""" + pattern = re.compile(r"(\[[^\]\n]+\]\(https?://[^)\s]+\)|\*\*[^*\n]+\*\*)") + out = [] + for part in pattern.split(text): + if not part: + continue + if part.startswith("[") and "](http" in part: + label, url = part[1:].split("](", 1) + if url.endswith(")"): + url = url[:-1] + out.append(f"[{escape_markdown_v2(label)}]({url})") + elif part.startswith("**") and part.endswith("**"): + out.append(f"*{escape_markdown_v2(part[2:-2])}*") + else: + out.append(escape_markdown_v2(part)) + return "".join(out) + + def format_number(value: float, decimals: int = 2) -> str: """Format a number with commas and specified decimals""" if value >= 1000000: From dd5bd4419598ead14c4de1d4f50794c908da0243 Mon Sep 17 00:00:00 2001 From: Laurent Grawet Date: Sun, 30 Aug 2026 14:43:31 +0200 Subject: [PATCH 2/4] (fix) address Greptile review: existing HTML anchors and bold-only results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _escape_cell / formatCellValue now also turn an existing cell (the pool scanner's Link cell) into a clickable link, with tag-free content only — raw tags still escape. - format_routine_result renders **bold**-only results outside the code block, so bold markers are no longer suppressed when no link is present. - Tests cover markdown links, HTML anchors, XSS safety, and bold-only Telegram rendering. Co-Authored-By: Claude Opus 4.7 --- condor/reports/builder.py | 18 ++++++++-- .../components/routines/RoutineResultView.tsx | 11 ++++++ tests/test_report_builder.py | 32 +++++++++++++++++ tests/test_routine_result_markdown.py | 35 ++++++++++++++++++- utils/telegram_formatters.py | 12 +++---- 5 files changed, 98 insertions(+), 10 deletions(-) diff --git a/condor/reports/builder.py b/condor/reports/builder.py index 734cbbdd5..b980b6e91 100644 --- a/condor/reports/builder.py +++ b/condor/reports/builder.py @@ -38,14 +38,20 @@ _CELL_LINK_RE = re.compile( r"^\[([^\]\n]+)\]\((https?://[^)\s]+)\)(\s*\S*)?$" ) +_HTML_ANCHOR_RE = re.compile( + r'^]*>(.*?)$', + re.DOTALL, +) def _escape_cell(value: object) -> str: - """Escape a table cell; a full ``[label](url)`` becomes a clickable link. + """Escape a table cell; a full ``[label](url)`` markdown link or an + existing ```` anchor becomes a clickable link. Only a cell whose whole value is a markdown link (an optional trailing - marker such as `` ⚠`` is allowed) is converted to an ````. Everything - else is escaped verbatim, so existing plain-text cells render unchanged. + marker such as `` ⚠`` is allowed) or a well-formed http(s) anchor with + tag-free content is converted to an ````. Everything else is escaped + verbatim, so existing plain-text cells render unchanged. """ text = str(value) m = _CELL_LINK_RE.match(text) @@ -55,6 +61,12 @@ def _escape_cell(value: object) -> str: f'target="_blank" rel="noopener noreferrer">' f"{html.escape(m.group(1))}{html.escape(m.group(3) or '')}" ) + m = _HTML_ANCHOR_RE.match(text) + if m and "<" not in m.group(2) and ">" not in m.group(2): + return ( + f'{m.group(2)}' + ) return html.escape(text) diff --git a/frontend/src/components/routines/RoutineResultView.tsx b/frontend/src/components/routines/RoutineResultView.tsx index c5749b480..c8dd79894 100644 --- a/frontend/src/components/routines/RoutineResultView.tsx +++ b/frontend/src/components/routines/RoutineResultView.tsx @@ -8,6 +8,7 @@ interface Props { } const CELL_LINK_RE = /^\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)(\s*\S*)?$/; +const HTML_ANCHOR_RE = /^]*>(.*?)<\/a>$/; function formatCellValue(val: unknown) { if (typeof val === "number") { @@ -28,6 +29,16 @@ function formatCellValue(val: unknown) { ); } + const m2 = HTML_ANCHOR_RE.exec(s); + if (m2 && !m2[2].includes("<") && !m2[2].includes(">")) { + // Existing HTML anchors (e.g. the pool scanner's Link cell) render as + // links too, mirroring ReportBuilder's _escape_cell. + return ( + + {m2[2]} + + ); + } return s; } diff --git a/tests/test_report_builder.py b/tests/test_report_builder.py index 77730f083..c50282a66 100644 --- a/tests/test_report_builder.py +++ b/tests/test_report_builder.py @@ -9,6 +9,7 @@ import condor.reports as reports from condor.reports import rendering, store +from condor.reports.builder import _escape_cell from condor.reports.footprint import ( build_estimated_footprint_figure, candle_timestamps, @@ -45,6 +46,37 @@ def test_markdown_preserves_single_newlines(): assert "Market Analysis" in rendered +def test_escape_cell_markdown_link_becomes_anchor(): + out = _escape_cell("[GeckoTerminal](https://www.geckoterminal.com/solana)") + assert 'href="https://www.geckoterminal.com/solana"' in out + assert 'target="_blank"' in out + assert ">GeckoTerminal" in out + + +def test_escape_cell_existing_html_anchor_passes_through(): + # The pool scanner already emits HTML anchors in its Link cell. + out = _escape_cell( + '🔗' + ) + assert 'href="https://www.geckoterminal.com/solana/pools/abc"' in out + assert 'target="_blank"' in out + assert "🔗" in out + + +def test_escape_cell_anchor_with_tags_is_escaped(): + # An anchor whose content carries raw tags must not pass through. + out = _escape_cell('ok') + assert "