diff --git a/condor/reports/builder.py b/condor/reports/builder.py
index a9fd47837..b980b6e91 100644
--- a/condor/reports/builder.py
+++ b/condor/reports/builder.py
@@ -35,6 +35,41 @@
}
+_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)`` 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) 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)
+ if m:
+ return (
+ f''
+ 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)
+
+
class ReportBuilder:
"""Compose a report section by section, then ``save()`` it.
@@ -561,7 +596,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..c8dd79894 100644
--- a/frontend/src/components/routines/RoutineResultView.tsx
+++ b/frontend/src/components/routines/RoutineResultView.tsx
@@ -7,6 +7,41 @@ interface Props {
instance: RoutineInstance;
}
+const CELL_LINK_RE = /^\[([^\]\n]+)\]\((https?:\/\/[^)\s]+)\)(\s*\S*)?$/;
+const HTML_ANCHOR_RE = /^]*>(.*?)<\/a>$/;
+
+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] ?? ""}
+ >
+ );
+ }
+ 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;
+}
+
interface KpiSection {
type: "kpi";
label: string;
@@ -141,7 +176,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/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 "