Skip to content

Tooltip with full name added to email html#6

Closed
crucisco wants to merge 2 commits intofuric:mainfrom
crucisco:feature/minor-email-tweaks
Closed

Tooltip with full name added to email html#6
crucisco wants to merge 2 commits intofuric:mainfrom
crucisco:feature/minor-email-tweaks

Conversation

@crucisco
Copy link
Copy Markdown

@crucisco crucisco commented Apr 8, 2026

Introduce tickerFullName (based on longName property on Yahoo quote). Used in HTML 'title' attribute to create a tooltip to easily identify stock or ETF. Useful when ticker/SEDOL is ambiguous or obscure.

@crucisco
Copy link
Copy Markdown
Author

crucisco commented Apr 8, 2026

Hi Richard
I'm impressed with what you've done with this app. I've taken the liberty of raising this PR - it's just a basic UX enhancement.

-Dai

@furic furic requested a review from Copilot April 27, 2026 15:16
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 a “full name” tooltip to ticker displays in the generated email HTML, using Yahoo quote metadata so ambiguous tickers are easier to identify.

Changes:

  • Add tickerFullName to allocation/alert/recommendation types and thread it through the analysis pipeline.
  • Render ticker full names via HTML title attributes across daily/weekly/intraday email templates.
  • Prefer Yahoo longName over shortName when populating quote name.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/weeklyEmail.ts Adds title tooltip for tickers in weekly rebalance table.
src/intradayEmail.ts Adds title tooltip for tickers in intraday alert email.
src/intradayCompare.ts Extends IntradayAlert with tickerFullName and passes it through.
src/fetchPrices.ts Changes quote name selection to prefer longName first.
src/email.ts Adds title tooltips for tickers across daily brief sections.
src/detailedAnalysis.ts Includes full name in detailed-analysis prompt and adds a new instruction about using it.
src/analyze.ts Extends AllocationItem with tickerFullName derived from quote.name.
src/aiAnalysis.ts Extends AIBuyRecommendation with tickerFullName and includes full name in AI prompt.
Comments suppressed due to low confidence (1)

src/aiAnalysis.ts:42

  • AIBuyRecommendation now requires tickerFullName, but the Gemini response schema (responseSchema) doesn’t include that field and there’s no post-processing that populates it. As a result, tickerFullName will be undefined at runtime for AI recs (JSON.parse won’t provide it), so the new email tooltips / prompts that rely on it won’t actually show the full name. Consider populating tickerFullName immediately after parsing (e.g., from report.items / priceData) and normalizing missing values to null so it also persists when saving baselines.
export interface AIBuyRecommendation {
  ticker: string;
  tickerFullName: string | null;
  action: string;
  confidence: number;
  reason: string;
  suggestedBuyValue: number;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/intradayEmail.ts
<div style="padding:14px 0;border-bottom:1px solid ${S.border};">
<div style="margin-bottom:6px;">
<span style="font-weight:bold;font-size:16px;color:#fff;">${a.ticker}</span>
<span style="font-weight:bold;font-size:16px;color:#fff;" title="${a.tickerFullName || a.ticker}">${a.ticker}</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.

a.tickerFullName is inserted into an HTML title attribute without escaping. Since this string ultimately comes from external quote data, characters like quotes or </& can break the attribute and potentially inject HTML into the email. Use proper HTML attribute escaping (encode & < > " ') before interpolating into title.

Copilot uses AI. Check for mistakes.
Comment thread src/email.ts
<div style="padding:10px 0;border-bottom:1px solid ${S.border};">
<div style="margin-bottom:4px;">
<span style="font-weight:bold;font-size:14px;color:#fff;">${rec.ticker}</span>
<span style="font-weight:bold;font-size:14px;color:#fff;" title="${rec.tickerFullName || rec.ticker}">${rec.ticker}</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.

rec.tickerFullName is interpolated directly into an HTML title attribute without HTML attribute escaping. Because the value can originate from external sources, this can break the markup or enable injection if it contains quotes / < / &. Add an attribute-escaping helper (encode & < > " ') and apply it here.

Copilot uses AI. Check for mistakes.
Comment thread src/email.ts
${others.map((rec) => `
<div style="padding:4px 0;font-size:12px;">
<span style="font-weight:bold;">${rec.ticker}</span>
<span style="font-weight:bold;" title="${rec.tickerFullName || rec.ticker}">${rec.ticker}</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.

This title attribute uses rec.tickerFullName without HTML attribute escaping. If the full name contains characters like quotes or </&, it can break the HTML and potentially inject content into email clients. Escape the value for attribute context (encode & < > " ').

Copilot uses AI. Check for mistakes.
Comment thread src/email.ts
${report.items.map((item) => `
<tr>
<td style="padding:5px 3px;border-bottom:1px solid ${S.border};font-weight:bold;">${item.ticker}</td>
<td style="padding:5px 3px;border-bottom:1px solid ${S.border};font-weight:bold;" title="${item.tickerFullName || item.ticker}">${item.ticker}</td>
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.

item.tickerFullName is inserted into an HTML title attribute without escaping. Because the value comes from external quote data, it should be HTML-attribute escaped (encode & < > " ') to avoid broken markup / injection in email clients.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed in 5ecfb07. Added the title attribute to the User ID span to provide the full identifier on hover, consistent with other truncated fields.

Comment thread src/weeklyEmail.ts
return `
<tr>
<td style="padding:5px 3px;border-bottom:1px solid ${S.border};font-weight:bold;">${item.ticker}</td>
<td style="padding:5px 3px;border-bottom:1px solid ${S.border};font-weight:bold;" title="${item.tickerFullName || item.ticker}">${item.ticker}</td>
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.

The title attribute is populated from item.tickerFullName (sourced from external Yahoo data) without HTML attribute escaping. If the name contains characters like ", <, &, it can break the markup or enable HTML injection in email clients. Please escape attribute values (at least & < > " ') via a small helper (e.g., escapeHtmlAttr) before interpolating into title (and consider applying it consistently to other attribute interpolations).

Copilot uses AI. Check for mistakes.
Comment thread src/email.ts
${buys.map((b) => `
<tr>
<td style="padding:6px 4px;border-bottom:1px solid ${S.border};font-weight:bold;">${b.ticker}</td>
<td style="padding:6px 4px;border-bottom:1px solid ${S.border};font-weight:bold;" title="${b.tickerFullName || b.ticker}">${b.ticker}</td>
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.

b.tickerFullName is inserted into an HTML attribute without escaping. Since it is derived from external quote metadata, it should be escaped for attribute context (encode & < > " ') to prevent malformed HTML or injection in email clients.

Copilot uses AI. Check for mistakes.
Comment thread src/aiAnalysis.ts
Comment on lines +210 to 214
const fullName = item.tickerFullName || item.ticker;

const lines = [
`${item.ticker}:`,
`${item.ticker} (${fullName}):`,
isShortBond ? ` Asset type: SHORT-DURATION BOND ETF (1-5 year, ~2% price range — apply framework 12a)` : null,
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.

fullName falls back to item.ticker, which makes prompt lines like SPY (SPY): when no full name is available. That’s harmless but adds noise to the prompt and can reduce clarity. Consider only adding the parentheses portion when item.tickerFullName is present and different from the ticker.

Copilot uses AI. Check for mistakes.
Comment thread src/detailedAnalysis.ts
Comment on lines 49 to 52
`You are a senior investment analyst writing a detailed buy recommendation for a client.`,
``,
`TICKER: ${ticker}`,
`TICKER: ${ticker}${rec.tickerFullName ? ` (${rec.tickerFullName})` : ""}`,
`Current price: $${quote.price.toFixed(2)}`,
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.

The prompt’s full-name display is sourced from rec.tickerFullName, but AI recommendations currently aren’t guaranteed to carry that field (it isn’t part of the Gemini decision schema). This means the detailed prompt may still omit the full name, conflicting with the new instruction to use it in the thesis. Prefer sourcing the full name from quote.name and/or report.items (which come from Yahoo quote data) rather than from the AI rec object.

Copilot uses AI. Check for mistakes.
@furic
Copy link
Copy Markdown
Owner

furic commented Apr 27, 2026

Hey @crucisco — thanks a lot for this contribution! The tooltip idea is genuinely useful and I've shipped it. After review, a few small issues came up that needed addressing before merge:

  1. HTML attribute injectiontitle="${rec.tickerFullName || rec.ticker}" was unescaped at six sites. A company name containing " or ' (e.g. "McDonald's Corporation") would break the attribute or open an injection vector.
  2. name priority swap in fetchPrices.ts — swapping shortName ?? longNamelongName ?? shortName affects fetchNews.ts:34-35, the only consumer of quote.name. News search queries would shift from "Apple" to "Apple Inc.", potentially reducing match quality.
  3. Trailing newlines removed from aiAnalysis.ts and email.ts.
  4. AI prompt rule numbered 0. rather than renumbering the existing list.

Rather than ask you for revisions, I went ahead and re-implemented the feature with these fixes on my own branch (fix/pr-6-tooltip-issues):

  • Added a shared escapeHtmlAttr helper in src/util.ts (covers & < > " ') used at every title= site.
  • Kept quote.name unchanged; added a separate quote.longName field that flows into tickerFullName. News-search behavior preserved.
  • The tickerFullName is attached to AI recommendations server-side from price data after the model returns — deterministic, not model-dependent.
  • AI prompts updated (full names in Stage 1 + a CRITICAL RULE in detailedAnalysis.ts).

I also took the opportunity to add what was missing in the first place — a CI gate (npm run typecheck + prettier --check + .editorconfig) so future external PRs get automatic style/type signal. That would have caught issues 3 and 4 before review.

I'm closing this PR as not-merged, but the feature lands credited to your idea. Thanks again — really appreciate you taking the time to send a patch. 🙏

@furic
Copy link
Copy Markdown
Owner

furic commented Apr 27, 2026

Superseded by branch fix/pr-6-tooltip-issues (commits 2ec69f9..dc79874). Thanks again @crucisco!

@furic furic closed this Apr 27, 2026
furic added a commit that referenced this pull request Apr 27, 2026
* feat: thread tickerFullName tooltip with HTML attribute escaping

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>

* chore: add prettier + editorconfig + reformat baseline

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>

* ci: add typecheck + format gate for external PRs

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>

* docs: add CI badge to README

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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@crucisco
Copy link
Copy Markdown
Author

Hi @furic
Thanks for taking on this enhancement and making it better! I appreciate it and look forward to future development.

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.

4 participants