Add ticker name tooltips + external-PR CI gate#9
Conversation
Adds full-name tooltips to email HTML by threading `tickerFullName` (sourced from Yahoo `longName`) through analyze → aiAnalysis → intradayCompare. Each title attribute is run through a new `escapeHtmlAttr` helper that handles & < > " ', preventing attribute-breaking when names contain quotes or apostrophes (e.g., "McDonald's Corporation"). Notable choices: - `quote.name` priority unchanged (`shortName ?? longName`) — preserves existing news-search behavior in fetchNews.ts. The new `quote.longName` field is sourced separately for tooltips and AI prompts. - `tickerFullName` is attached to AI recommendations server-side from priceData after Stage 2, not requested from the model — deterministic and immune to model fabrication. - AI prompts (Stage 1 + detailedAnalysis) instruct Gemini to reference tickers by full name in reasoning text, improving readability across email, Telegram, and detailed thesis output. - Refresh email also gains a tooltip via `quote.longName` directly. Refactor: existing `escapeHtml` in telegram.ts moved to a shared `src/util.ts` exporting both `escapeHtmlAttr` (5-char) and `escapeHtmlText` (3-char). Supersedes external PR #6 with HTML-attribute-safe escaping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds tooling so future external PRs get automatic style enforcement: - .editorconfig — 2-space indent, LF line endings, final newline. Most modern editors auto-respect this, which would have caught the trailing- newline regressions we saw on a recent external PR. - .prettierrc.json — semi, double quotes, trailing commas, 100-col width. - .prettierignore — skips node_modules, state/, docs/, package-lock. - package.json — adds `typecheck`, `format`, `format:check` scripts and `prettier` devDependency. - One-time prettier baseline reformat applied to all src/*.ts files so `format:check` passes on a clean checkout. No runtime behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds .github/workflows/ci.yml that runs on `pull_request` and `push` to main. Runs `npm ci → npm run typecheck → npm run format:check`. Why pull_request (not pull_request_target): forks get the fork's code and an unprivileged GITHUB_TOKEN, so untrusted code never executes with write access or repo secrets. The portfolio-monitor workflow stays secret-gated; this CI workflow needs no secrets at all. Catches: type errors, prettier drift, missing trailing newlines (via .editorconfig in editors + prettier in CI). Open PRs from forks now get a green/red signal automatically — none existed before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the existing portfolio-monitor + docs badges. Placed first since it's the most relevant signal of repo health for fork contributors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds full company-name tooltips for tickers (using Yahoo longName) across email/intraday outputs and nudges Gemini prompts to reference companies by full name, plus introduces basic CI gating for type/style consistency on PRs.
Changes:
- Introduces shared HTML escaping helpers (
escapeHtmlAttr,escapeHtmlText) and uses them for ticker tooltiptitleattributes. - Threads
tickerFullNamefrom price data through analysis → AI recs → intraday alerts and updates prompts to use full names in reasoning. - Adds Prettier/EditorConfig config and a CI workflow to run
tsc --noEmit+ Prettier check.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/util.ts |
Adds shared HTML escaping helpers for attributes and text nodes. |
src/fetchPrices.ts |
Adds QuoteData.longName sourced from Yahoo quote summary. |
src/analyze.ts |
Adds tickerFullName to AllocationItem sourced from QuoteData.longName. |
src/aiAnalysis.ts |
Adds tickerFullName to AI rec type; includes full name in prompt and attaches name deterministically from price data post-response. |
src/intradayCompare.ts |
Adds tickerFullName to intraday alerts, propagated from AI recs. |
src/email.ts |
Adds ticker title tooltips in daily email (plus Prettier reformat). |
src/intradayEmail.ts |
Adds ticker title tooltips in intraday + refresh emails (plus Prettier reformat). |
src/weeklyEmail.ts |
Adds ticker title tooltips in weekly email (plus Prettier reformat). |
src/telegram.ts |
Replaces local HTML escaping with shared helper; Prettier reformat. |
src/detailedAnalysis.ts |
Includes full name next to ticker in detailed prompt; adds instruction to use it in thesis. |
src/fetchNews.ts |
Prettier-only changes (formatting, trailing commas, line breaks). |
src/fetchTechnicals.ts |
Prettier-only changes (formatting, trailing commas, line breaks). |
src/guards.ts |
Prettier-only changes (formatting, trailing commas, line breaks). |
src/state.ts |
Prettier-only changes (formatting, trailing commas, line breaks). |
src/index.ts |
Prettier-only changes (formatting, trailing commas, line breaks). |
src/config.ts |
Prettier-only changes (formatting, trailing commas, line breaks). |
package.json |
Adds typecheck and Prettier scripts + Prettier dev dependency. |
package-lock.json |
Locks Prettier dependency. |
README.md |
Adds CI workflow badge. |
.editorconfig |
Adds EditorConfig baseline for consistent formatting in editors. |
.prettierrc.json |
Adds Prettier configuration. |
.prettierignore |
Adds ignore rules for Prettier. |
.github/workflows/ci.yml |
Adds CI job running npm ci, typecheck, and Prettier check on PRs/pushes. |
Comments suppressed due to low confidence (1)
src/telegram.ts:104
- Telegram messages are sent with
parse_mode: "HTML", butrec.reasonis embedded directly inside an HTML tag (<i>...</i>) without escaping. If the model outputs&,<, or>(or accidentally includes tags), the message formatting can break or allow unintended markup. ApplyescapeHtmlText(...)torec.reason(and other free-text fields likelimitPriceReason/bottomSignal) before embedding.
lines.push(` <i>${rec.reason}</i>`);
// Technical insight for STRONG BUY only
if (rec.action === "STRONG BUY") {
const tech = technicals[rec.ticker];
if (tech) {
lines.push(
` 📈 ${tech.momentumSignal} · RSI ${tech.rsi14} · 50MA $${tech.sma50} (${tech.priceVsSma50 > 0 ? "+" : ""}${tech.priceVsSma50}%)` +
(tech.macdCrossover
? ` · MACD ${tech.macdCrossover}`
: tech.macdHistogram != null
? ` · MACD hist ${tech.macdHistogram > 0 ? "+" : ""}${tech.macdHistogram}`
: "") +
(tech.bollPercentB != null ? ` · %B ${tech.bollPercentB}` : "") +
(tech.bollSqueeze ? " · 🔸squeeze" : ""),
);
}
if (rec.suggestedLimitPrice && rec.suggestedLimitPrice > 0) {
lines.push(
` 💡 Limit: $${rec.suggestedLimitPrice.toFixed(2)}` +
(rec.limitPriceReason ? ` — ${rec.limitPriceReason}` : ""),
);
}
if (rec.bottomSignal && rec.bottomSignal !== "") {
lines.push(` 🔻 Bottom: ${rec.bottomSignal}`);
}
if (rec.analysisUrl) {
lines.push(` 📋 <a href="${rec.analysisUrl}">More Details</a>`);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: "20" |
There was a problem hiding this comment.
CI sets up Node.js 20, but the repo’s other workflows (e.g. portfolio-monitor) and README indicate Node 22+. This can hide runtime/type differences between CI and production. Update this workflow to use the same Node major version as the rest of the repo (or derive from a single source, e.g. .nvmrc).
| node-version: "20" | |
| node-version: "22" |
| <div style="font-size:12px;color:${S.text};margin-top:4px;">${rec.reason}</div> | ||
| ${buildTechnicalInsight(rec, technicals[rec.ticker])} | ||
| ${rec.action === "STRONG BUY" && rec.analysisUrl ? ` | ||
| ${ | ||
| rec.action === "STRONG BUY" && rec.analysisUrl | ||
| ? ` | ||
| <div style="margin-top:8px;"> | ||
| <a href="${rec.analysisUrl}" style="display:inline-block;background:${S.blue}22;color:${S.blue};padding:4px 12px;border-radius:4px;font-size:11px;font-weight:bold;text-decoration:none;border:1px solid ${S.blue}44;">More Details →</a> |
There was a problem hiding this comment.
rec.reason (and other model-supplied strings like limitPriceReason / bottomSignal) are interpolated directly into the email HTML without escaping, and rec.analysisUrl is injected into an href without attribute escaping. If any of these contain &, <, > or quotes, the email markup can break or allow HTML injection. Escape all untrusted text with escapeHtmlText(...) and all attribute values/URLs with escapeHtmlAttr(...) before embedding in HTML.
| <div style="margin-left:12px;margin-bottom:4px;font-size:12px;"> | ||
| <a href="${a.url}" style="color:${S.blue};text-decoration:none;">→ ${a.title}</a> | ||
| <span style="color:${S.muted};font-size:11px;"> — ${a.source}</span> |
There was a problem hiding this comment.
News items (a.url, a.title, a.source) come from an external API but are embedded into the email HTML without escaping. This can break rendering (common with & in titles/URLs) and is an injection risk. Use escapeHtmlAttr(...) for the href value and escapeHtmlText(...) for a.title/a.source text content.
| <div style="font-size:12px;color:${S.text};margin-bottom:4px;">${a.reason}</div> | ||
| ${a.suggestedBuyValue > 0 ? `<div style="font-size:13px;font-weight:bold;color:#fff;">Suggested: ${fmt$(a.suggestedBuyValue)}</div>` : ""} | ||
| ${a.currentAction === "STRONG BUY" && a.suggestedLimitPrice && a.suggestedLimitPrice > 0 ? `<div style="font-size:12px;color:${S.green};margin-top:4px;">Limit order: $${a.suggestedLimitPrice.toFixed(2)}${a.limitPriceReason ? ` — ${a.limitPriceReason}` : ""}</div>` : ""} | ||
| ${a.bottomSignal && a.bottomSignal !== "" ? `<div style="font-size:11px;color:${S.yellow};margin-top:4px;">Bottom signal: ${a.bottomSignal}</div>` : ""} | ||
| ${a.currentAction === "STRONG BUY" && a.analysisUrl ? `<div style="margin-top:8px;"><a href="${a.analysisUrl}" style="display:inline-block;background:#3498db22;color:#3498db;padding:4px 12px;border-radius:4px;font-size:11px;font-weight:bold;text-decoration:none;border:1px solid #3498db44;">More Details →</a></div>` : ""} |
There was a problem hiding this comment.
Several fields in the intraday email HTML are interpolated without escaping (a.reason, a.limitPriceReason, a.bottomSignal, and a.analysisUrl in href). These values are model-supplied or external and can contain characters that break HTML or allow injection. Escape text nodes with escapeHtmlText(...) and attribute values/URLs with escapeHtmlAttr(...) before embedding.
| const headline = articles[0]; // 1 per ticker | ||
| const line = `<b>${ticker}:</b> <a href="${headline.url}">${escapeHtml(headline.title)}</a>\n`; | ||
| const line = `<b>${ticker}:</b> <a href="${headline.url}">${escapeHtmlText(headline.title)}</a>\n`; | ||
| if (newsText.length + line.length > budgetForNews) break; |
There was a problem hiding this comment.
headline.url is inserted into an <a href="..."> in a Telegram HTML message without attribute escaping. URLs commonly contain & (query params) which should be encoded as & in HTML attributes, and quotes would break the attribute entirely. Use escapeHtmlAttr(...) (or otherwise properly encode) for headline.url before embedding in href.
Summary
tickerFullName(YahoolongName) through the pipeline so email tickers get hover tooltips, and Gemini reasoning text references companies by full name. Supersedes Tooltip with full name added to email html #6 with HTML-attribute-safe escaping (escapeHtmlAttrcovers& < > " ').Changes
Feature (commit 2ec69f9):
src/util.ts— sharedescapeHtmlAttr(5-char) andescapeHtmlText(3-char).telegram.tsnow imports from it.quote.longNamefield — keptquote.namepriority unchanged (shortName ?? longName) so news-search behavior infetchNews.tsis preserved.tickerFullNameplumbed throughanalyze.ts→aiAnalysis.ts→intradayCompare.ts. Attached to AI recs server-side from price data, not from the model — deterministic.title=interpolations escaped inemail.ts,intradayEmail.ts,weeklyEmail.ts. Refresh email also gets a tooltip viaquote.longName.detailedAnalysis.ts) instruct Gemini to use full names in reasoning.Tooling (commit 7c6521f):
.editorconfig,.prettierrc.json,.prettierignorepackage.json— addstypecheck,format,format:checkscripts +prettierdevDepCI (commit dc79874):
.github/workflows/ci.yml— runs onpull_requestandpushto main:npm ci → typecheck → format:check. No secrets, nopull_request_target(so untrusted fork code never runs with privileged tokens).Docs (commit 84ebad9):
Test plan
npm run typecheck— passes locallynpm run format:check— passes locallyvalidatejob, 13s)scratch/smoke-tooltip.ts, untracked) — fedMCDwith"McDonald's Corporation"plus an adversarialFoo" onmouseover="alert(1)name throughbuildEmailHtml. Verified rendered HTML containstitle="McDonald's Corporation"at every site (AI rec + allocation table + fallback table) and the injection attempt becomestitle="Foo" onmouseover="alert(1)"— quote escaped, no live event handler.npm run devend-to-end — Yahoo prices for 18/19 tickers, technicals for 19/19, macro indicators OK. Gemini was 503ing (Google capacity), so fallback gap-based recs path exercised — that's also a tooltip site, confirming wiring works on the fallback branch too. Email + Telegram delivered.🤖 Generated with Claude Code