Skip to content

Add ticker name tooltips + external-PR CI gate#9

Merged
furic merged 4 commits intomainfrom
fix/pr-6-tooltip-issues
Apr 27, 2026
Merged

Add ticker name tooltips + external-PR CI gate#9
furic merged 4 commits intomainfrom
fix/pr-6-tooltip-issues

Conversation

@furic
Copy link
Copy Markdown
Owner

@furic furic commented Apr 27, 2026

Summary

  • Threads tickerFullName (Yahoo longName) 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 (escapeHtmlAttr covers & < > " ').
  • Adds a CI gate (typecheck + Prettier + EditorConfig) so external PRs get automatic style/type signal — none existed before.

Changes

Feature (commit 2ec69f9):

  • New src/util.ts — shared escapeHtmlAttr (5-char) and escapeHtmlText (3-char). telegram.ts now imports from it.
  • New quote.longName field — kept quote.name priority unchanged (shortName ?? longName) so news-search behavior in fetchNews.ts is preserved.
  • tickerFullName plumbed through analyze.tsaiAnalysis.tsintradayCompare.ts. Attached to AI recs server-side from price data, not from the model — deterministic.
  • 6 title= interpolations escaped in email.ts, intradayEmail.ts, weeklyEmail.ts. Refresh email also gets a tooltip via quote.longName.
  • AI prompts (Stage 1 + detailedAnalysis.ts) instruct Gemini to use full names in reasoning.

Tooling (commit 7c6521f):

  • .editorconfig, .prettierrc.json, .prettierignore
  • package.json — adds typecheck, format, format:check scripts + prettier devDep
  • One-time prettier baseline reformat across all 16 src files

CI (commit dc79874):

  • .github/workflows/ci.yml — runs on pull_request and push to main: npm ci → typecheck → format:check. No secrets, no pull_request_target (so untrusted fork code never runs with privileged tokens).

Docs (commit 84ebad9):

  • README badge for the new CI workflow.

Test plan

  • npm run typecheck — passes locally
  • npm run format:check — passes locally
  • CI runs on this PR and goes green (validate job, 13s)
  • Synthetic smoke test (scratch/smoke-tooltip.ts, untracked) — fed MCD with "McDonald's Corporation" plus an adversarial Foo" onmouseover="alert(1) name through buildEmailHtml. Verified rendered HTML contains title="McDonald&#39;s Corporation" at every site (AI rec + allocation table + fallback table) and the injection attempt becomes title="Foo&quot; onmouseover=&quot;alert(1)" — quote escaped, no live event handler.
  • npm run dev end-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.
  • Post-merge: open a test fork PR, confirm CI auto-triggers and gates style/type drift

🤖 Generated with Claude Code

furic and others added 4 commits April 28, 2026 01:30
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>
@furic furic self-assigned this Apr 27, 2026
@furic furic requested a review from Copilot April 27, 2026 15:41
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 tooltip title attributes.
  • Threads tickerFullName from 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", but rec.reason is 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. Apply escapeHtmlText(...) to rec.reason (and other free-text fields like limitPriceReason / 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.

Comment thread .github/workflows/ci.yml

- uses: actions/setup-node@v4
with:
node-version: "20"
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
node-version: "20"
node-version: "22"

Copilot uses AI. Check for mistakes.
Comment thread src/email.ts
Comment on lines 187 to 193
<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 &rarr;</a>
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/email.ts
Comment on lines 368 to 370
<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>
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/intradayEmail.ts
Comment on lines 124 to 128
<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 &rarr;</a></div>` : ""}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/telegram.ts
Comment on lines 144 to 146
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;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 &amp; in HTML attributes, and quotes would break the attribute entirely. Use escapeHtmlAttr(...) (or otherwise properly encode) for headline.url before embedding in href.

Copilot uses AI. Check for mistakes.
@furic furic merged commit dfa5b0a into main Apr 27, 2026
5 checks passed
@furic furic deleted the fix/pr-6-tooltip-issues branch April 27, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants