Tooltip with full name added to email html#6
Conversation
|
Hi Richard -Dai |
There was a problem hiding this comment.
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
tickerFullNameto allocation/alert/recommendation types and thread it through the analysis pipeline. - Render ticker full names via HTML
titleattributes across daily/weekly/intraday email templates. - Prefer Yahoo
longNameovershortNamewhen populating quotename.
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
AIBuyRecommendationnow requirestickerFullName, but the Gemini response schema (responseSchema) doesn’t include that field and there’s no post-processing that populates it. As a result,tickerFullNamewill beundefinedat 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 populatingtickerFullNameimmediately after parsing (e.g., fromreport.items/priceData) and normalizing missing values tonullso 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.
| <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> |
There was a problem hiding this comment.
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.
| <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> |
There was a problem hiding this comment.
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.
| ${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> |
There was a problem hiding this comment.
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 & < > " ').
| ${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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in 5ecfb07. Added the title attribute to the User ID span to provide the full identifier on hover, consistent with other truncated fields.
| 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> |
There was a problem hiding this comment.
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).
| ${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> |
There was a problem hiding this comment.
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.
| 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, |
There was a problem hiding this comment.
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.
| `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)}`, |
There was a problem hiding this comment.
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.
|
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:
Rather than ask you for revisions, I went ahead and re-implemented the feature with these fixes on my own branch (
I also took the opportunity to add what was missing in the first place — a CI gate ( 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. 🙏 |
* 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>
|
Hi @furic |
Introduce
tickerFullName(based onlongNameproperty 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.