v0.4.1

Changelog

Track the evolution of NavViewTZ

Have an idea?

Help shape the future of NavViewTZ

Request a Feature
2026-08-28

Comparator: Growth Metrics Matrix

The /compare page (FundComparison.vue) previously only showed a line chart β€” no way to see how two funds actually stacked up without eyeballing the chart. Brought it closer to parity with DSEasy's comparator.

Added

  • Growth Metrics table below the chart: for each selected fund, 1M / 3M / 6M / 1Y growth (computed from history via nearest-date matching, same approach as DSEasy's CompareTickers.tsx), plus growth/peak/trough for whatever period is currently selected in the sidebar β€” labelled Selected (90d) etc. so the window is never ambiguous. πŸ† marks the better fund per row.
  • 30-day volatility and max drawdown rows, sourced from volatility_30d / max_drawdown β€” already computed server-side (functions/src/enrich.performanceMetrics.ts) and used in the monthly email briefing, but never surfaced anywhere in the frontend until now. Verified live against Firestore: populated on all 24 funds. Volatility is intentionally colored neutral (not green/red) and wins on the lower value β€” the one metric where "less" is the good direction.
  • Custom HTML legend replacing Chart.js's canvas-drawn one: each fund's actual logo, ringed in its series color with a light matching tint over the logo itself (not the plate β€” an early version tinted the white background instead and made dark-colored logos disappear in dark mode), at a larger font than the old default.
  • Growth Metrics table headers get the same treatment (logo + colored ring) instead of a plain color dot.
2026-08-28

Sidebar Cleanup

Removed

  • Manual "Refresh Data" sidebar button (src/components/Sidebar.vue) β€” redundant with the automatic refresh App.vue already triggers (fundsStore.refresh(true)); removed the button, its isRefreshing spin state, and the now-unused useFundsStore import from the sidebar. The store's own refresh() method is untouched β€” only the manual UI trigger is gone.
2026-08-27

WebMCP Surface and Firestore Fallbacks

Seven read-only WebMCP tools now expose fund research to in-browser agents, three of which drive the comparison and calculator pages through query parameters. Building the deep links surfaced a class of silent failure: the pages looked correct while showing nothing.

Fixed

  • Deep-linked comparisons never loaded history (src/components/FundComparison.vue) β€” fund ids seeded from the query during setup did not trigger the NAV history watchers, so a cold load rendered the right funds, period and toggle over a permanently empty chart. Seeding moved into onMounted. The same change removes a hydration mismatch logged on every deep-linked load, which the existing 52-test hydration suite missed because it only visits query-less URLs.

  • Fund ids were compared case-sensitively β€” the catalogue, static data files and agent surface all published itrust.icash while the Firestore document is itrust.iCash, so a link built from any published id left the fund selector blank. Ids now resolve case-insensitively on both the comparison and calculator pages, and the catalogue is aligned to the Firestore spelling. Firestore document paths are case-sensitive, so the same mismatch had also made the history export read a nonexistent path and write no snapshot for iCash at all.

  • A Firestore outage rendered blank pages with no explanation β€” an unreachable Firestore does not reject; it resolves an offline-cache read as an empty snapshot, which is indistinguishable from "no data". Both the fund list and NAV history had Firestore as their only source.

Added

  • Deployment snapshots as a fallback (scripts/generate-fund-history.mjs) β€” 400 days of NAV per fund exported to public/data/history/ during prebuild, and fetchLatest falls back to the existing public/data/latest.json. Together these keep the selectors populated and the charts drawn when Firestore is unavailable.

  • Saved figures are labelled (src/components/SnapshotNotice.vue) β€” the store records whether data came from a live read or a snapshot, and the dashboard, analysis, calculator and comparison pages show the as-of date when serving saved data, so it is never presented as current.

  • check:fund-ids in prebuild (scripts/check-fund-ids.mjs) β€” a catalogue id whose casing differs from its Firestore document id now fails the build instead of silently producing a blank selector and an empty export. Funds catalogued before their first Firestore document warn rather than fail.

Notes

  • WebMCP requires #enable-webmcp-testing in Chrome 151; without it document.modelContext is undefined and no tool registers. Tool behaviour is covered against an injected harness in tests/webmcp.spec.ts, not a real agent host.
  • The fallback path is proven against the built bundle in test, not against production, since exercising it there means breaking Firestore.
2026-08-22

Monthly Digest: Fix Blank Pipeline Activity

The Monthly Digest email (introduced 2026-06-21) had been sending with "Pipeline Runs / Snapshots Written / Errors Logged" all showing 0 and no indication anything was wrong β€” confirmed live via triggerMonthlyDigest for July's digest.

Fixed

  • Missing composite index for the pipeline activity aggregate (firestore.indexes.json) β€” gatherMonthlyDigest's count()/sum() aggregate query over pipeline_logs (filtered by timestamp range) was throwing FAILED_PRECONDITION: The query requires an index on every run. The surrounding try/catch swallowed it and defaulted to 0/0/0, so the email always looked "sent successfully" even though that section never loaded. Source reliability and coverage used different queries and were unaffected, which is why only the activity numbers were blank.

Added

  • sectionErrors on the digest (functions/src/reports.monthlyDigest.ts) β€” each data-gathering step (pipeline activity, source health, funds/ coverage) now records its failure instead of just logging a warning. A red "πŸ›‘ Data Collection Failed" banner renders in the email when any section fails, the subject/status icon reflects it instead of showing "βœ… Clean month," and triggerMonthlyDigest's JSON response includes the real error β€” this is what made the missing index diagnosable in minutes instead of requiring log archaeology.
2026-08-12

v0.4.1 MCP Guardrail Fixes from Live ChatGPT Evaluation

Addresses the 4 open failures from the 2026-08-11 live ChatGPT evaluation (docs/submission/evidence/2026-08-11/evaluation-report.md). All four are prompt/description/output-text guardrails plus one structural widget fix; none can be confirmed against the real ChatGPT host without a fresh live evaluation run.

Added

  • MCP server instructions (functions/src/mcp/server.ts) β€” a server-wide guidance string (surfaced via client.getInstructions()) telling the model to always call a tool for current figures, resolve fund_ids from get_market_briefing/search_funds before calling compare_funds, never call compare_funds more than once per request and merge results into a larger ranking (max 5 funds per call), and never recommend a fund when a comparison is incompatible.
  • Structured error card β€” errorResult() now returns structuredContent: {error, disclaimer} alongside the existing error text, and the widget (functions/src/mcp/ui.ts) renders an "Unable to complete request" card instead of staying blank. Fixes the empty output-template iframe ChatGPT showed above the unknown-fund error response. Doesn't apply to SDK-level input-validation errors (e.g. the 5-fund-limit rejection), which never reach the widget.

Changed

  • compare_funds incompatible-comparison warning β€” both the structuredContent.warning and the model-facing text now explicitly say "do not recommend one fund over another, or say which one you would choose or buy," closing the gap where the widget correctly blocked an incompatible comparison but the model still verbally recommended a fund.
  • Tool descriptions (functions/src/mcp/contracts.ts) β€” strengthened get_market_briefing, search_funds, and compare_funds with explicit discovery-before-comparison and 5-fund-cap/no-chaining guidance.
  • SKILL.md (plugins/navviewtz/skills/tanzania-fund-research/) β€” same guardrails reinforced at the skill layer: no chaining compare_funds calls across a single request, and an explicit no-recommendation instruction for the comparable: false case.

2026-08-11

v0.4.0 Public MCP Server for ChatGPT & Agents

Publishes NavViewTZ's read-only fund data as an MCP (Model Context Protocol) server at https://www.navviewtz.com/mcp, so ChatGPT and other MCP-aware agents can query Tanzania fund data directly instead of scraping the site or the static JSON exports.

Added

  • navViewMcp Firebase function (functions/src/mcp/) β€” a stateless Streamable HTTP MCP server exposing four read-only tools: get_market_briefing, search_funds, get_fund_details, compare_funds, plus a compact HTML UI resource for ChatGPT's app surface. Reads a single cached public_exports/latest_funds Firestore aggregate; capped at 3 instances / 30s timeout.
  • headline_metric_status (comparable / stale / insufficient) β€” threaded through the enrichment pipeline (enrich.performanceMetrics.ts), public exports, and the static agent-surface generator, so rankings and MCP tools only ever compare funds on genuinely current, same-basis metrics instead of silently mixing in stale or missing ones.
  • plugins/navviewtz β€” a Codex/ChatGPT plugin manifest pointing at the MCP endpoint.
  • functions/scripts/check-mcp-endpoint.mjs and docs/mcp-operations.md β€” a remote health check and the release/monitoring runbook for this endpoint.

Fixed

  • enrichFund() headline carry-forward β€” a force re-enrich could null out performance_basis/headline_metric for funds with a dedicated collector (Sanlam's published-yield feed), because the in-memory enrichment record used to build the public export didn't reflect what the Firestore merge: true write actually preserved. Added existingHeadlineFields() to carry those fields forward explicitly, with unit coverage.
  • /mcp routing β€” the SPA's Edge Middleware (middleware.ts) was intercepting /mcp and returning its own 404 before the vercel.json rewrite to the Firebase function could run; /mcp is now excluded from the middleware's matcher.

2026-07-26

Indexable Swahili Pages (closes #56)

Publishes the Swahili experience at its own crawlable URLs. sw.json already covered the app UI, but the Swahili pages had no URL of their own and the crawlable shells rendered by middleware.ts were English-only, so from a search engine's point of view the Swahili site did not exist.

Added

  • /sw locale tree - every public page is mirrored at a /sw prefix (/sw, /sw/compare, /sw/funds/utt-liquid, /sw/managers/itrust-finance). Routes are declared once and mirrored programmatically in src/router/index.ts, so a new route is covered in both locales automatically.
  • Swahili crawlable copy - the middleware's prerendered shells, page titles, meta descriptions, table labels, and JSON-LD descriptions are translated (~106 strings). Terminology follows the register already established in sw.json: mfuko/mifuko, mapato for NAV return and mazao for published yield, kitengo, ada, ukwasi, NAV kept as-is.
  • hreflang annotations - every page emits reciprocal en, sw, and x-default alternates server-side, and public/sitemap.xml carries the matching xhtml:link blocks (88 URLs across 2 locales).
  • Locale-aware regression checks - check-seo-middleware.mjs now asserts Swahili rendering, lang="sw", prefix-internal links, Swahili 404s, and hreflang reciprocity in both the responses and the sitemap.

Changed

  • Locale resolution - a /sw URL now outranks localStorage and the browser default, so a visitor arriving from a Swahili search result is not flipped back to English.
  • Language switcher - switching locale navigates to the same page in the other tree (/compare ↔ /sw/compare) instead of leaving the URL contradicting the rendered language.
  • Fund profile intro - uses the curated catalogue category instead of the raw structure field, fixing the ungrammatical "as a Open-end Money Market" in English and giving Swahili a translated noun phrase.

2026-06-28

v0.3.2 Phase 3 Technical Maturity Completed

Marks Phase 3 as complete after the final User Insight & Analytics work landed.

Changed

  • Roadmap Phase 3 - moved from "Current Focus" to "Completed" now that sections 3.1-3.6 are all checked off.
  • Phase 4 handoff - Phase 4 Personalization & Ecosystem is now the next roadmap focus.

2026-06-28

Optional GA4 Interaction Tracking (closes roadmap Β§3.5 Interaction Tracking)

Adds a lightweight analytics boundary that is silent unless VITE_GA_MEASUREMENT_ID is configured.

Added

  • src/lib/analytics.ts - initializes GA4/gtag on demand, emits SPA route page views, and exposes trackEvent() plus a v-analytics directive for declarative click tracking.
  • Feature-flow events - feedback opens, subscription success/failure, calculator fund/projection changes, comparison fund selections, comparison window/normalize changes, and comparison PNG/CSV exports.

Changed

  • Roadmap Β§3.5 - Interaction Tracking is now marked complete; Feedback Mechanism was already complete.
  • README - documents the optional VITE_GA_MEASUREMENT_ID deployment setting and the privacy boundary for tracked events.

All notable updates and features for NavViewTZ are documented here.


2026-06-21

Reporting Overhaul: Monthly Digest, Source Uptime Metrics & Distinct Senders

Reworks the Cloud Functions reporting layer so the admin gets a clear daily digest, a dedicated monthly retrospective, and reliability metrics β€” with each email channel visually distinct in the inbox.

Added

  • Source uptime/downtime metrics (functions/src/sourceHealth.ts) β€” A durable per-source reliability ledger (meta/source_health) accumulated from incident transitions. Each resolved incident becomes a downtime episode; summarizeWindow() computes per-source uptime %, incident count, and total downtime over any window. Fed automatically from runIncidentCheck, so no extra pipeline work. Unit-tested in src/__tests__/sourceHealth.test.ts.
  • Monthly Digest report (functions/src/reports.monthlyDigest.ts) β€” New monthlyDigestReport scheduled function (1st of month, 7am EAT) emailing a retrospective of the previous calendar month: pipeline activity totals, a least-reliable-first source uptime leaderboard, snapshot coverage (gaps/duplicates/pending), and funds stale at month close. Includes a triggerMonthlyDigest HTTPS endpoint for on-demand previews.
  • Daily report header summary β€” The daily QA email header now carries an at-a-glance issue summary line (e.g. "3 errors Β· 2 stale Β· 1 schema change", or "No issues found").

Changed

  • Daily report schedule β€” Moved from 0 8 * * 1-5 (8am, Mon–Fri) to 0 7 * * * (7am, every day) so weekend NAVs are reported on.
  • Distinct email senders (functions/src/config.ts MAIL_SENDERS) β€” Reports now send as "NavViewTZ Reports", incident/schema alerts as "NavViewTZ Alerts", and the subscriber weekly pulse as "NavViewTZ Insights", so each channel is filterable in the inbox.

Removed

  • Day-5 monthly-coverage block on the daily email β€” superseded by the standalone monthly digest. The old block silently skipped any month whose 5th fell on a weekend (the daily only ran Mon–Fri).

2026-06-20

In-App Feedback Link (closes roadmap Β§3.5 Feedback Mechanism)

Routes user feedback through the existing Google Form already wired up from the changelog page, instead of standing up a separate form/backend or sending users to GitHub Issues.

Added

  • src/lib/externalLinks.ts β€” single source of truth for off-site URLs. Currently exports FEEDBACK_FORM_URL; future additions (WhatsApp invite, social links) belong here too.
  • Send feedback footer link (SiteFooter.vue) β€” opens the existing Google Form in a new tab (target="_blank", rel="noopener noreferrer"), uses the lucide MessageSquarePlus icon to match the existing footer-link visual pattern. Single entry covers both feature requests and bug reports β€” the form's own categorisation handles routing.

Changed

  • ChangelogView.vue β€” switched its inline FEATURE_REQUEST_FORM_URL constant to import FEEDBACK_FORM_URL from @/lib/externalLinks, so the URL lives in exactly one place. The existing "Request a Feature" CTA on the changelog page is unchanged behaviour-wise.
  • Footer link nav β€” switched from gap-8 to gap-6 with flex-wrap so the new entry lays out cleanly alongside Subscribe / WhatsApp / Methodology / About across the responsive breakpoints.

Verified

  • npm run build passes β€” no new warnings.

2026-06-20

v0.3.1 Sticky Mobile Header & Managers Catalogue Polish

Refactors the mobile layout with a unified, sticky inline top header and page-aware title system. Polishes the Managers Catalogue page with sort controls, structured category tags, and layout enhancements.

Added

  • Sticky Mobile Header β€” Replaced the floating hamburger button with a 56px sticky top bar inside DashboardLayout.vue, containing the hamburger inline with the page title.
  • Dynamic Mobile Titles (usePageTitle) β€” Introduced a usePageTitle composable to dynamically set the mobile header title based on the active view (e.g. manager or fund name), with unmount cleanup to prevent title clobbering.
  • Managers Catalogue Sorting β€” Added a Sort By control to the /managers page matching the homepage pattern (Name, Fund count, Earliest fund) with ascending/descending toggles.
  • Managers Catalogue Metric Strip β€” Added a top metric strip under the header summarizing total managers, tracked funds, and the earliest inception date.
  • Fund Category Badges β€” Derived a categories[] list from each manager's funds to render category tags on manager cards.
  • Local Dev Launch Config β€” Added .claude/launch.json defining launch profiles for Vite dev, preview, and Firebase functions emulator.

Changed

  • Mobile Viewport Optimization β€” Hidden in-page H1 headers on mobile for dashboard-managed views (Home, Fund Grid, Analysis, Compare, Calculator, Reports, Managers) to maximize vertical space.
  • Managers List & Cards Design β€” Replaced prose summary with category chips, structured manager stats into distinct pills, and added hover lift animation effects.
  • High-Contrast Brand Plates β€” Placed manager logos on a white card background plate to ensure readability of dark transparent logo glyphs in dark mode.
  • Tabulated Growth Scrolling β€” Made the main growth tab strip horizontally scrollable to prevent viewport overflow on narrow screens.
  • Methodology Header Polish β€” Adjusted Methodology page sticky header z-index to z-20 to render beneath the sidebar menu.

2026-06-20

Per-Fund Enrichment Dirty Flag (closes roadmap Β§3.3)

Closes the last open item under Pipeline Reliability. enrichAllFunds now distinguishes funds whose NAV history actually changed this run (full re-enrichment) from funds that didn't (light-touch consensus refresh), cutting steady-state Firestore reads from enrichment by ~95% per the audit projection.

Added

  • EnrichOptions.advancedFunds: Set<string> (functions/src/enrich.performanceMetrics.ts) β€” opt-in scoping that gates which funds get the expensive snapshot-read + metric-recomputation path. When omitted, all funds full-enrich (legacy behaviour for triggerEnrichment and cleanAndRebackfillUTT).
  • classifyFundForEnrichment(...) β€” pure helper exporting the full | light decision rule so the gate is exercisable without a Firestore emulator. Handles legacy mode, advanced membership, never-enriched first-run defence, and empty/undefined sets.
  • Light-touch pass β€” funds not in advancedFunds get their existing /funds/{id} doc patched with the three consensus-relative fields (consensus_date, days_behind_consensus, freshness_status) and pass through into /meta/fund_summary with cached metrics intact. No snapshot read, no /enriched/{date} write.
  • functions/src/__tests__/enrich.partition.test.ts β€” 12 unit tests pinning the classifier's behaviour across legacy mode, advanced membership, never-enriched fallback, empty sets, undefined sets, and realistic overnight / post-deploy / manual-trigger scenarios.

Changed

  • syncFundData pipeline (functions/src/index.ts) β€” when enrichment runs, passes advancedFunds = new Set(persistedFunds) so only funds that actually persisted new rows trigger a full recompute. The outer hasAdvanced gate (skip enrichment entirely when zero funds advanced) is unchanged.
  • enrichAllFunds log lines now report the full vs. light split each run, so steady-state reduction is visible in Cloud Logging without bespoke metrics.
  • /meta/fund_summary correctness preserved β€” every fund still contributes, so widget/dashboard reads stay single-document. Funds that didn't advance contribute cached metrics overlaid with the latest consensus snapshot.

Verified

  • npm test in functions/ β€” 115 / 115 tests pass (12 new partition tests, 14 existing calendar tests, 89 pre-existing).
  • npm run build in functions/ β€” esbuild bundle compiles cleanly (157.2 kb).
  • Pre-existing reports.weeklyPulse.ts:131 typecheck failure (unrelated Array.prototype.at lib target) is unchanged.

2026-06-20

Tanzania Holiday Calendar for d1 Metrics

Closes roadmap Β§3.1 by replacing the weekend-only prevCanonicalWorkingDay with prevPublicTanzaniaWorkingDay, a holiday-aware step that drives the canonical_start field surfaced with d1 metrics.

Added

  • TZ_PUBLIC_HOLIDAYS set (functions/src/enrich.performanceMetrics.ts) β€” gazetted Tanzania public holidays for 2026, covering New Year, Zanzibar Revolution Day, both Eid el-Fitr days, Good Friday, Easter Monday, Karume Day, Union Day, Labour Day, Eid al-Adha, Saba Saba, Nane Nane, Maulid, Mwalimu Nyerere Day, Independence Day, Christmas, and Boxing Day.
  • prevPublicTanzaniaWorkingDay(dateStr) β€” bounded backward walk that skips both weekends and gazetted holidays; degrades transparently to weekend-only logic for dates outside the loaded years (no regression risk).
  • functions/src/__tests__/enrich.calendar.test.ts β€” 14 unit tests across normal weekdays, weekend traversal, single-day clusters (Saba Saba, Nyerere Day, Eid al-Adha), multi-day clusters (Easter+Karume = 5-day gap, Eid el-Fitr weekend, Christmas weekend), the 2025β†’2026 boundary, and out-of-coverage degradation.

Changed

  • calcD1Return canonical_start derivation now reflects the real expected previous trading day during holiday clusters (e.g. Wed 8 Apr 2026 β†’ Thu 2 Apr 2026, stepping over the full Good Friday + Easter Monday + Karume cluster), instead of mislabelling Tue 7 Apr.
  • Doc comments throughout enrich.performanceMetrics.ts updated to reference the new helper and the gazetted calendar; the "future hook" wording removed.
  • PERIOD_TOLERANCE_DAYS justification clarified β€” tolerances stay unchanged for now and will be revisited based on observed STALE outcomes over a full quarter rather than calendar-coverage alone.

Verified

  • npm test in functions/ β€” 103 / 103 tests pass (including the 14 new calendar tests).
  • Pre-existing typecheck failure in reports.weeklyPulse.ts:131 is unrelated to this change.

2026-06-20

v0.3.0 Managers Catalogue, Weekly Reports Redesign & Sorting Controls

Aggregates a complete fund managers profile catalogue with local branding assets, modernizes the weekly report dashboard layout and list chronology, and introduces interactive card-ordering controls and table-sorting indicators.

Added

  • Managers Catalogue & Detailed Profiles (/managers & /managers/:managerSlug) β€” Full directory of the 9 fund managers, mapped to high-quality local logo assets in public/logos/, with custom summaries, earliest tracked inception dates, and dynamic managed fund cards.
  • Homepage Card Ordering Controls β€” Added a compact, inline sorting panel next to the Deep Dive Cards header, allowing users to sort cards by Fund name, Manager, Latest date, NAV price, 1W return, YTD return, and 1Y return, including a square toggle button to switch between ascending/descending order.
  • Sortable Weekly Report Tables β€” Interactive headers for the weekly report table (Fund, NAV, NAV/Unit, 1 Week, YTD) that display sort-direction icons and toggle order when clicked.

Changed

  • Weekly Report Layout Redesign β€” Removed custom page-level margins and email-style wrappers from the weekly report details page, refactoring the view into dashboard-native containers while maintaining print/export capabilities.
  • Newest-First Weekly Report Sorting β€” Normalized the weekly reports archive list to always sort newest-first using Firestore timestamps and fallback heuristics for incomplete records.
  • Branding & Logo Integrations β€” Mapped manager profiles to their respective high-quality local logo PNG files in public/logos/ instead of fetching live site favicons.
  • Homepage Actions Polish β€” Routed homepage table action buttons directly to /analysis?fund=<fund_id> to preselect the clicked fund on the analysis view.

2026-06-18

v0.2.2 Fund Pages & Discovery

Adds dedicated in-app fund and manager pages, strengthens internal navigation from the dashboard and showcase, and lays the groundwork for search discovery with crawler-friendly metadata and sitemap assets.

Added

  • Fund profile pages (/funds/:fundId) - Dedicated pages for each tracked fund with NAV context, returns, fees, liquidity, benchmark, operating notes, and official source links.
  • Manager profile pages (/managers/:managerSlug) - In-app landing pages that group tracked funds by manager and link back into fund profiles.
  • Shared fund presentation mapper (src/data/fundPresentation.ts) - Centralized fund/manager route IDs, slugs, metadata merging, and presentation shaping for cards and profile pages.
  • Crawler entry points - Added public/robots.txt and public/sitemap.xml with fund, manager, and core app URLs.

Changed

  • Internal fund navigation - Fund names and CTAs in the showcase now open NavViewTZ fund pages instead of sending users straight to external manager sites.
  • Dashboard links - Fund names and manager names in Table Summary, Top/Bottom Movers, Deep Dive Cards, and Performance Heatmap now route to in-app pages.
  • Route metadata - Extended Vercel middleware metadata for /funds/... and /managers/..., including canonical URL injection and fund-specific titles/descriptions.
  • Vercel rewrites - Excluded robots.txt and sitemap.xml from the SPA catch-all rewrite so crawlers can fetch them directly.

2026-06-18

Fund Logo Fallback & Layout Polish

Implements high-fidelity fallback UX for fund logos when specific fund logos are missing or fail to load. This release adds the official Vertex logo, falls back to manager logos with custom CSS initials badges inside the logo circle, dynamically generates colorful HSL gradient backgrounds based on the fund ID or name when manager logos are also missing, and hides the badge on small UI footprints to prevent clipping or distortion.

Added

  • Vertex International Securities logo (public/logos/vertex.png) β€” Added the official logo.
  • Dynamic HSL gradients β€” Implemented helper gradientStyle in FundLogo.vue to generate high-quality, professional linear HSL gradients deterministically from the fund name or ID.

Changed

  • FundLogo.vue fallback hierarchy β€” Updated component logic:
    1. Try loading specific fund logo (/logos/<fundId>.png).
    2. Fall back to manager logo (/logos/<providerId>.png) with a small initials badge at the bottom-right.
    3. Fall back to full-container initials with a modern gradient background if the manager logo is also missing.
  • Initials deduplication β€” Added a explicit initials lookup map (FUND_INITIALS_MAP) to prevent identical initials for different funds (e.g. ZI for Ziada vs. ZI+ for Ziada Insured).
  • Adaptive layout sizing β€” Hidden the initials badge and centered the manager logo for very small container sizes (anything smaller than h-8 like the h-5 Select Fund sidebar logo) to avoid clipping and text distortion.
  • Double concentric borders fix β€” Cleaned up wrapper classes to eliminate nested double border lines passed down from parent components.

2026-06-16

Orbit Collector: Drop Livewire, Use Static Panels

Orbit's redesign now renders both fund tables (money_market, dozen_index) as static data-history-panel blocks in the initial page HTML β€” no Livewire round-trip required. This release rips out the CSRF / livewire/update POST flow entirely and slices the two panels apart with a bounded extraction so they can't bleed into each other.

Changed

  • collectOrbit() β€” Reduced from 3 sequential HTTP calls (page GET + 2 Livewire POSTs) to a single page GET. Removed CSRF extraction, Livewire snapshot parsing, session-cookie forwarding, and the Livewire POST helpers. The historical-table location strategies (id β†’ heading β†’ most-rows-wins) are unchanged.
  • Panel isolation (extractSegment) β€” Slices each panel from its data-history-panel="<name>" marker up to the next data-history-panel= marker so a panel can't pick up tables from an adjacent panel. The leading-space requirement scopes the match to HTML attributes and ignores inline-script string literals that mention the same attribute name.
  • Missing-panel fingerprint β€” When a panel is absent the collector emits a sentinel fingerprint (MISSING_PANEL:<name>) alongside the COLLECTION FAILED alert, so the layout canary will fire when a panel reappears with a different structure.
  • spot-check.mjs β€” Orbit checks now probe for the two data-history-panel markers and β‰₯2 NAV-shaped tables in the initial HTML instead of CSRF token / Livewire snapshot probes.

Notes

  • The CSRF/Livewire fallback ("a failed Livewire call no longer aborts the whole collector", added 2026-06-15) is moot now that the Livewire codepath is gone, but the COLLECTION FAILED alert contract it introduced is preserved on the missing-panel branches.

2026-06-15

Source-Change Monitoring & Orbit Collector Hardening

Orbit's site changed its NAV table structure (collapsing to a DATE + NET ASSET VALUE layout) and the collector silently produced zero rows for ~a month without raising any alert. Root cause: the existing schema check only fires when a table is found and parsed; a moved/missing table returned empty with no signal. This release closes that blind spot and makes the Orbit scraper resilient to layout changes.

Added

  • Collection-gap detection (detectCollectionGaps) β€” Any fund we actively collect that has prior history but returns zero rows in a run now raises an immediate SchemaAlert, routed through the existing same-run email path. Guards against false positives: only funds in the configured collectible set and with existing snapshots are flagged (discontinued/never-published funds are ignored).
  • Layout canary (detectLayoutChanges) β€” Each HTML source's header-row signature is fingerprinted per run and compared against the previous run (stored in /meta/source_fingerprints). Structural changes that do not break column mapping (reorders, additions, cosmetic rewrites) now trigger an early-warning alert before data silently degrades.
  • Orbit collector fallbacks β€” Locates the historical NAV table by id β†’ "Historical NAV Table" heading β†’ header-signature (largest matching table), and falls back to the server-rendered initial page HTML when the Livewire response yields no table.

Changed

  • collectOrbit() β€” A failed Livewire call (e.g. HTTP 419) no longer aborts the whole collector; it is caught, fallbacks are attempted, and an explicit COLLECTION FAILED alert is raised when no table can be found. Whatever recognised columns are present (at minimum DATE + Net Asset Value) are ingested rather than dropping the fund entirely.
  • CollectorResult β€” Gained an optional fingerprints map (fund_id β†’ header signature) for the layout canary.
  • Immediate alert email β€” Generalised from "Schema Change Detected" to "Source Alert" to cover schema changes, layout changes, and stopped-data conditions.

Added

  • persistedFunds tracking in pipeline logs β€” The syncFundData pipeline now captures and stores exactly which fund IDs received new snapshots during a run.
  • Enhanced "Funds Updated" list in Daily QA Report β€” The morning report now uses the new persistedFunds data to provide a 100% accurate list of updated funds per run, including the specific time they were updated. This replaces the previous heuristic and ensures that if a run fails or skips a fund, it is correctly reflected in the report.

Changed

  • transformAndPersist() β€” Now returns an object {count, fundIds} instead of a single number, providing better downstream visibility for the pipeline.
  • gatherDailyStats() β€” Refactored to leverage persistedFunds for the "Funds Updated" table in the daily email digest.

2026-04-30

Functions Audit & Maintenance Documentation

Added

  • scripts/audit-functions.mjs β€” New comprehensive audit script for Google Cloud Functions and Cloud Run.
    • Fetches pipeline run success rates from Firestore pipeline_logs.
    • Pulls Cloud Monitoring metrics (invocations/errors) and Cloud Logging entries.
    • Estimates infrastructure costs via cost proxy.
    • Generates structured markdown reports in docs/reports/.
  • scripts/README.md β€” Central documentation for all maintenance, diagnostic, and asset management scripts.

Changed

  • Agent.md β€” Added "Weekly Maintenance Tasks" section to formalize the use of audit scripts.

2026-04-23

Scraper Reliability Fixes: fetchWithRetry & Orbit CSRF

Root cause analysis of two recurring scraper failures seen across multiple pipeline runs on Apr 23. Both WHI and Orbit scrapers failed on every run despite target websites being online. Identified and fixed two bugs β€” one critical (affects all scrapers), one Orbit-specific.

Fixed

  • utils.ts β€” Broken retry logic in fetchWithRetry (Critical, affects ALL scrapers)

    The AbortController and its timeout were created once and shared across all retry attempts. Once the 30s timeout fired (or the first fetch encountered a network error that aborted the signal), every subsequent retry was instantly killed because an aborted signal stays aborted forever. This made the retry mechanism a no-op for any request that failed on the first try.

    Additionally, the clearTimeout() call was only in the success path β€” on failure, the timer leaked and could fire mid-retry-sleep, poisoning the next attempt.

    Fix: Create a fresh AbortController + setTimeout per attempt, and clearTimeout in both success and error paths.

    BeforeAfter
    1 AbortController for all 4 attempts1 AbortController per attempt
    Timeout fires once, kills all retriesTimeout scoped to each attempt
    Timer leaked on catchTimer cleared on catch
  • collect.orbit.ts β€” CSRF token never sent to Laravel (Orbit-only, caused HTTP 419)

    buildHeaders() set X-Livewire, Content-Type, User-Agent, Referer, and Cookie β€” but never set X-CSRF-Token. Then livewirePost() tried to read headers["X-CSRF-Token"] for the request body _token field, which was always undefined, falling back to extractTokenFromSnapshot() which returned the Livewire snapshot checksum β€” not the Laravel CSRF token. Laravel correctly rejected every POST with HTTP 419 "Page Expired".

    Fix:

    1. buildHeaders() now includes "X-CSRF-Token": csrf in the returned headers object
    2. livewirePost() now receives csrf as a direct parameter and uses it for the body _token field, eliminating the fragile header-lookup indirection

Verified

  • TypeScript build: βœ… clean (esbuild, 87.1kb bundle)
  • Unit tests: βœ… 42/42 passed (all 5 test suites including collect.orbit.test.ts)
  • Live site checks: βœ… Both whi.go.tz and orbit.co.tz responding normally

Core Purpose

NavViewTZ is a modern, real-time performance dashboard designed for Tanzania's mutual funds ecosystem. It empowers investors, fund managers, and financial enthusiasts to:

  • Track Performance β€” Monitor live market cap, ROI, and key fund metrics at a glance
  • Analyze Trends β€” Explore historical data with time-series charts and momentum tracking
  • Compare Funds β€” Make informed decisions with side-by-side fund comparison tools
  • Simulate Investments β€” Project wealth growth and contribution outcomes with an interactive calculator
  • Stay Informed β€” Access weekly performance reports and deep insights

Tech Stack: Vue 3, Vite, Chart.js, TailwindCSS, Google Sheets (backend β†’ migrating to Firebase/Firestore)


2026-04-23

Orbit Collector Fix, Backfill & Gap-Aware Staleness

Fixed

  • collect.orbit.ts broken Livewire component β€” Orbit split their page into two components: net-asset-value (live summary) and historical-net-asset-value (historical table). The collector was calling toggleTable on the wrong component, causing HTTP 500 since at least Apr 21. Fixed by targeting historical-net-asset-value for the Livewire interaction. Collector is working again β€” no data loss on existing Firestore data.

  • spot-check.mjs smoke check β€” updated to verify historical-net-asset-value instead of net-asset-value, so a future component rename will be caught at deploy time.

  • collect.orbit.test.ts fixtures β€” updated component name in both the HTML fixture and the Livewire response mock.

Added

  • scripts/backfill-orbit.mjs β€” one-off backfill script that fetches the full Orbit historical table with no depth limit, diffs against Firestore, and writes only missing dates. First run filled 44 gaps: 13 for orbit.inuka and 31 for orbit.inukaDozen (AppScript-era single-row scraping holes and the recent Apr 21–22 collector outage).

  • Gap-aware staleness in staleness.ts β€” getStalenessMap now scans the last PIPELINE.GAP_SCAN_ROWS (60) snapshots per fund instead of just the latest one. If any consecutive gap exceeds PIPELINE.GAP_THRESHOLD_DAYS (5 days), daysStale is bumped to cover back to the gap edge. This feeds deriveFetchLimit which then returns a deep enough window to fill the gap on the next collector run β€” self-healing without manual intervention.

  • PIPELINE.GAP_SCAN_ROWS: 60 added to config.ts.

⚠️ Reverted β€” Gap-Aware Staleness

The limit(60) gap scan in getStalenessMap was reverted in commit 08fda80. It was over-engineered: the Livewire collector fix and the backfill-orbit.mjs one-off already resolved all known gaps, making the per-run scan unnecessary. The pipeline is back to limit(1) for staleness detection. Gap monitoring responsibility moved to the monthly coverage check (see below).


2026-04-23

Stage 3 Migration Complete & Monthly Coverage Check

Migration: Stage 3 Complete

  • scripts/validate-stage3.mjs β€” fixed a field name mismatch: the validator was reading return_1m, return_3m, sparkline from Firestore but the enrichment function writes ret_1m, ret_3m, sparkline_1y. All 16 funds now pass Stage 3 validation (16/16 βœ…).
  • Shadow week running until 2026-04-29. Stage 4 (flip frontend to Firestore) begins after that date.

Added

  • Monthly coverage check in the 5th-of-month daily email (functions/src/reports.dailyEmail.ts) β€” On the 5th of each month, the morning QA email gains an extra block summarising the previous calendar month's snapshot coverage for all 16 funds. For each fund it reports: gap count (consecutive gaps > GAP_THRESHOLD_DAYS), duplicate date count (data integrity violations), and a "pending" flag for slow publishers whose latest snapshot predates the 28th. Implemented by querying each fund's snapshots subcollection with limit(35); no new Cloud Function required.

2026-04-23

Firestore Audit Expanded to All 16 Funds

Changed

  • scripts/firestore-check.mjs β€” expanded from 7 funds (iTrust Γ— 6 + Zansec) to all 16 funds across every collector. Funds are now grouped by manager for readable output, run in parallel via Promise.all, and report a summary table at the end.

    New funds added: whi.faida, orbit.inuka, orbit.inukaDozen, utt.bond, utt.jikimu, utt.liquid, utt.umoja, utt.watoto, utt.wekeza

    ChangeDetail
    Per-fund stale thresholdWHI gets 7 days (government fund); all others 3 days
    Parallel Firestore readscount(), recent snapshots, and oldest-date queries run concurrently per fund
    Exit code1 on any hard error (missing fund doc / empty subcollection); 0 on warnings-only or clean
    Summary tablePrinted at end with total errors, total warnings, and per-fund breakdown

    First live run results: 16 funds audited, 0 errors, 19 warnings β€” all warnings are the expected Easter 2026 holiday gap (Apr 2 β†’ Apr 8) and historical Orbit gaps from before the collector was wired up.


2026-04-23

Live Smoke Pre-Deploy Gate

Added

  • scripts/spot-check.mjs expanded to all 5 data sources β€” previously only covered iTrust and Zansec. Now verifies every upstream source before each deploy:

    SourceCheck
    iTrust (JSON API)All 6 expected fields present; dates parse as MM/DD/YYYY; core numeric fields non-null
    Zansec (HTML table)table#datatable present; all 6 expected column headers match; latest 3 rows parse cleanly
    WHI (HTML table)table#example1 present; all 6 expected column headers match; latest 3 data rows parse cleanly (footer rows filtered by digit-presence test)
    UTT AMIS (DataTables API)Page loads and CSRF token present; /navs API returns rows; at least one sname maps to a known scheme; latest date parses as DD-MM-YYYY
    Orbit (Livewire)Page loads; data-csrf attribute present; wire:snapshot for the net-asset-value component is present
  • Exit code gate β€” script now exits 1 on any hard error, blocking the Firebase deploy. Warnings (extra columns, date spread) are logged but do not block.

  • Parallel execution β€” all 5 source checks run concurrently via Promise.all; total wall time β‰ˆ 20–30 s.

  • Per-source summary table β€” printed at the end of every run so pass/fail is visible at a glance even when check output is interleaved.

Changed

  • firebase.json β€” added "node scripts/spot-check.mjs" as the third predeploy step (after lint and build). Any source-site structural change now blocks the deploy rather than silently producing null NAV data after it goes live.

2026-04-22

Fixture-Based Unit Tests for All Collectors

Added

  • Vitest test suite (functions/src/__tests__/) β€” 42 unit tests across 5 files covering all four collectors and the schema check module. Tests run offline against static HTML/JSON fixtures and require no network access or Firebase credentials.

    FileTestsCoverage
    schemaCheck.test.ts15All 4 pure check functions β€” valid data returns null, renamed/missing columns return correct alerts
    collect.iTrust.test.ts6API field mapping, MM/DD/YYYY β†’ YYYY-MM-DD parsing, fetch-depth limiting, schema alert on field rename
    collect.htmlTable.test.ts10WHI + Zansec parsing from live fixture HTML, staleness-driven row depth, schema alert on column rename
    collect.orbit.test.ts5Livewire 3-step flow with stubbed responses, DD-MM-YYYY parsing, schema alert on column rename
    collect.uttamis.test.ts6Scheme name mapping, DD-MM-YYYY parsing, schema alert for unrecognised scheme, deduplication of repeated unknown names
  • HTML/JSON fixtures (functions/src/__tests__/fixtures/) β€” Five static fixtures seeded from live sources. Run node scripts/fetch-fixtures.mjs to refresh the WHI and Zansec fixtures when source sites change; Orbit is hand-crafted from the expected #historicalNAVTable structure.

  • scripts/fetch-fixtures.mjs β€” Helper script to re-fetch WHI and Zansec HTML fixtures from the live sites.

Changed

  • functions/package.json β€” Added "test": "vitest run" script; ESLint --ignore-pattern src/__tests__ to exclude test files from the lint pass
  • functions/tsconfig.json β€” Excludes src/__tests__ so tsc --noEmit doesn't apply noUnusedLocals to test files
  • functions/vitest.config.ts β€” Minimal Vitest config targeting src/__tests__/**/*.test.ts

2026-04-22

Schema Change Alerting & Pipeline Diagnostics

Added

  • Schema change detection (functions/src/schemaCheck.ts) β€” New module with pure validation functions for each data source. Checks every collector run against configured column/field expectations and emits structured SchemaAlert objects (fund_id, collector, missing, actual, detected_at).

    Collectors covered:

    • iTrust β€” validates JSON field names in API response (checkITrustRecord)
    • HTML Table funds (WHI, Zansec) β€” validates all configured column titles against actual table headers; previously only threw on a missing date column and silently produced nulls for everything else (checkHtmlTableHeaders)
    • Orbit β€” validates #historicalNAVTable column headers after Livewire interaction (checkOrbitHeaders)
    • UTT AMIS β€” detects unrecognised scheme names, meaning a new or renamed UTT fund has appeared (checkUttSchemeNames)
  • Immediate schema alert email (functions/src/reports.dailyEmail.ts: sendSchemaChangeAlert) β€” If any schema change is detected during a pipeline run, an alert email fires within that same run rather than waiting for the 8am digest. Subject line: πŸ”΄ NavViewTZ: Schema Change Detected β€” [fund_ids]. Body lists each affected fund, the missing columns, the actual columns found, and detection timestamp.

  • Schema changes section in the daily digest β€” The morning QA email now includes a πŸ”΄ Schema Changes Detected block when alerts were logged in the last 24 h. Schema alerts also flip the overall status indicator from green to red.

  • schemaAlerts field in pipeline logs β€” Each /pipeline_logs/{id} document now stores a schemaAlerts array alongside errors, enabling historical audit of when schema changes first appeared.

  • Diagnostic scripts (scripts/) β€” Two standalone Node 22 ESM scripts for manual health checks (no build step required):

    • spot-check.mjs β€” Fetches live data from the iTrust API and Zansec HTML table, validates source schema and parse quality end-to-end
    • firestore-check.mjs β€” Connects to Firestore via service account key and audits snapshot coverage, date ranges, null fields on recent rows, and date gaps per fund

Changed

  • All four collector functions (collectITrust, collectUTTAMIS, collectHtmlTable, collectOrbit) now return CollectorResult { snapshots, schemaAlerts } instead of plain RawSnapshot[]
  • syncFundData now declares GMAIL_USER and GMAIL_APP_PASS secrets so it can send immediate schema alerts without depending on the dailyQAReport function

2026-04-22

Android Widget Support & Publish Lag Metrics

Added

  • Widget static metadata (functions/src/index.ts) β€” seedFundMetadata now includes five new fields per fund: fund_name_short, fund_manager_short, logo_key, color_primary, redemption_days. Enables the Android widget to render brand-correct UI from a single Firestore document read with no subcollection queries.

  • sparkline_7d (functions/src/enrich.performanceMetrics.ts) β€” 7-point native number[] array of the last 7 daily NAV values, oldest-first. Written to both /funds/{id} and /enriched/{date}. Distinct from sparkline_1y (50-point yearly overview stored as JSON string) β€” the 7-day version is a native Firestore array for direct Flutter List<double> consumption without JSON decoding.

  • Publish lag metrics (functions/src/enrich.performanceMetrics.ts) β€” Three new fields measuring time from NAV effective date @ 16:00 EAT to our scraper's collected_at, stored in hours (float):

    • latest_publish_lag β€” lag for the most recent snapshot
    • avg_publish_lag_7d β€” rolling mean of last 7 valid snapshots
    • avg_publish_lag_30d β€” rolling mean of last 30 valid snapshots

    Written to both /funds/{id} and /enriched/{date}. Null when fewer than 3 snapshots have a valid collected_at. Measures "time from market close to our awareness" β€” not the fund manager's actual publish time.

  • Android widget data reference (docs/widget_data_field_reference.md in NavViewTZ_Android) β€” Master reference document for the widget covering every required field, its Firestore source, Dart type, and whether it is read directly, computed on-device, or stored locally. Includes a complete FundWidgetModel.fromFirestore() factory.

Changed

  • seedFundMetadata manager names corrected to official legal names: "iTrust Finance Ltd", "UTT Asset Management and Investor Services PLC", "Orbit Securities Company Limited", "Watumishi Housing Investments", "Zan Securities Ltd".
  • Brand colours updated to match official fund family branding.

Fixed

  • sparkline_7d missing from enriched archive write β€” sparkline_7d was computed and written to the top-level fund doc in commit b7aab9b but omitted from the /enriched/{date} subcollection write. Fixed in 3c03467.

Unreleased

In Development:

  • Frontend confidence badges β€” inline ~ or colour dot (EXACT/APPROXIMATE/STALE) per return figure
  • Frontend tooltip/detail layer β€” surface actual_days, nav_start, nav_end, canonical_start, confidence per metric; use STALE to show "⚠ based on NAV 6 days apart β€” holiday period"
  • /methodology page linking to docs/metrics-methodology.md
  • Comparison matrix updated to use ret_*_ear fields explicitly

2026-04-20

Functional Design Token Architecture v0.2.0

Key Changes

  • Systematic Theming Migration β€” Converted hundreds of hardcoded Tailwind color scales and inline hex codes into a unified set of functional CSS variables (surface-base, content-primary, primary, success, danger, etc.).
  • Unified Dark Mode β€” Achieved full visual consistency across light and dark modes by mapping all components to a single source of truth in base.css.
  • Refactored Views & Components β€” Systematically migrated core views (Analysis, Reports, Subscriptions, Developer Tools) and specialized data displays (Comparison Matrix, Growth Tables, Accumulation Summary).

Technical Polish

  • Lean Design Tokens β€” Purged legacy Vue/Vite boilerplate variables, resulting in a minimal and purpose-built :root config.
  • Fixed Semantic Mismatches β€” Resolved structural issues where components used incorrect variables, ensuring the design system flows correctly from the root.

2026-04-19

NAV Lookup Methodology Overhaul & Enriched History

Changed

  • d1 Previous Trading Day Strategy (enrich.performanceMetrics.ts/js) β€” d1 no longer targets ago(1) as a calendar date. It now finds the second-most-recent NAV by index β€” the actual previous trading day regardless of calendar distance. After a Thu+Fri holiday + weekend, Monday correctly compares against Wednesday, not a failed lookup against Friday.

  • Period-aware tolerance β€” replaced the flat T=3 tolerance applied to all periods with a documented per-period config PERIOD_TOLERANCE_DAYS (w1=5, m1=7, m3=10, ytd=12, y1=14). Each value is justified against the worst-case Tanzania holiday gap (Thu+Fri+Sat+Sun = 4 days) with explicit reasoning in code.

  • STALE confidence level β€” no longer hard-refusing calculations beyond tolerance. NAVs found beyond PERIOD_TOLERANCE_DAYS but within a 30-day hard cap are calculated and flagged STALE rather than discarded. Confidence ladder is now EXACT β†’ APPROXIMATE β†’ STALE β†’ INSUFFICIENT.

  • prevCanonicalWorkingDay β€” new helper that returns the last Mon–Fri calendar day, used as the d1 canonical target for documentation and metrics_meta. Explicitly marked as the future hook for prevPublicTanzaniaWorkingDay once a Tanzania public holiday calendar is available.

  • metrics_meta schema extended β€” d1 now includes canonical_start (the prevCanonicalWorkingDay result) alongside start, end, days, nav_s, nav_e, conf. Frontend can use the gap between canonical_start and start to explain holiday-driven confidence downgrades.

  • Weekend cron (functions/src/index.ts) β€” scheduler extended from Mon–Fri (0 */3 * * 1-5) to daily (0 */3 * * *). Weekend runs exit cheaply when no new snapshots are found; enrichment is gated on transformAndPersist returning new data.

  • Both enrich.performanceMetrics.ts (Cloud Functions) and enrich.performanceMetrics.js (Apps Script) kept in full sync throughout.

Added

  • Enriched metrics history subcollection β€” enrichAllFunds now writes to /funds/{id}/enriched/{ret_as_of} in addition to the existing /funds/{id} merge. Doc ID = consensus date β†’ idempotent. Stores all computed metrics + enriched_at timestamp. Static fund metadata excluded (lives on the parent doc). Enables metric trend tracking, retroactive NAV audit, and future historical views.

  • EnrichedMetricsSnapshot type β€” TypeScript interface for the historical enriched record.

  • Firestore indexes (firestore.indexes.json) β€” two composite indexes for the new enriched subcollection: per-fund date-descending, and cross-fund by consensus_date + freshness_status.


2026-04-18

Build Fixes & Vercel Deployment

Fixed

  • getPeriodExplanation missing export (src/utils/interestRates.ts) β€” function was imported by DevInterestRatesView.vue but never exported; added implementation mapping each Period to a human-readable description.
  • Firebase package corruption β€” node_modules/firebase/ directory existed without a package.json, causing TypeScript to fail on firebase/firestore and firebase/auth subpath imports. Resolved via clean reinstall.
  • DevInterestRatesView.vue stale type references β€” view was using removed/renamed ReturnResult properties (isValid, dataPoints, latestDate, referenceDate); updated to current fields (value !== null, actualDays, endDate, startDate). Also typed periodLabels as Partial<Record<Period, string>> since the 3m period is intentionally excluded from this view.
  • firestoreService.ts non-null narrowing β€” snapshotSnap.docs[0] flagged as possibly undefined despite !empty guard; added non-null assertions at two call sites.
  • Email log target sheet (appscripts/lib.emailLog.gs) β€” logEmailBatch_() was incorrectly writing to the History spreadsheet; corrected to write to the Email List spreadsheet.

Deployment

  • Vercel preview deployed β€” First successful Vercel build after resolving all TypeScript and rollup errors above.

2026-04-18

Premium Branding & Logo Refinement

Added

  • New App Branding Assets (public/logo.png, public/logo-dark.png, public/logo-white.png) β€” Deployed premium high-resolution graphics for the NavViewTZ identity.
  • Theme-Aware Sidebar Logos (src/components/Sidebar.vue) β€” Implemented dynamic logo switching: dark graphic for light mode, white graphic for dark mode.

Fixed

  • Logo Transparency β€” Applied surgical pixel-level alpha channel correction to logo.png and all derived favicons to remove "fake" white corners and ensure clean rendering on any background.

Changed

  • Favicon Ecosystem β€” Re-generated all 7 standard favicon assets (.ico, 32x32, 16x16, apple-touch, android-chrome) from the new branded and corrected source.
  • Metadata & SEO β€” Updated index.html meta tags (og:image, twitter:image, og:title) to use the new branding and title consistency.
  • Sidebar Cleanup β€” Deprecated and removed the legacy vector-based IconTanzania.vue component in favor of the new branded raster assets.

2026-04-18

Dual-Track Returns, Email Corrections & Infrastructure

Fixed

  • EAR mislabeling bug β€” ret_1w (and all ret_* fields) were storing Effective Annual Rates since the April 14 metrics rewrite, causing the weekly email to display annualized figures (e.g. +105.76%) labeled as weekly returns. Root cause: safeVal_() / safeVal() always extracted .value (EAR) not .simple.

Changed

  • Dual-track return storage (appscripts/enrich.performanceMetrics.js, functions/src/enrich.performanceMetrics.ts) β€” ret_1d/1w/1m/3m/ytd/1y now store simple returns (actual period gain). Six new ret_*_ear fields store the annualized equivalents for cross-period comparison. Both Apps Script and Firebase Functions enrichment updated in sync.

  • Performance Heatmap (src/components/dashboard/PerformanceHeatmap.vue) β€” switched to ret_*_ear fields since it displays all periods side-by-side. Column headers now explicitly labeled p.a. All other components (LatestFundData, DailyPulse, DeepInsightCards, PulseHero, WatchlistWidget) continue using simple ret_* fields.

  • Weekly email template (appscripts/reports.performance.gs):

    • Removed "Here's the new spiced up snapshot" greeting line
    • AI summary markdown (**bold**, *italic*) now stripped and converted to HTML before rendering β€” no more raw asterisks visible to readers
    • Replaced purple left-border blockquote with a clean neutral card
    • Footer updated: removed "NavViewTZ Pipeline Agent" line, fixed broken href="#" unsubscribe, added "View Report Online" link, split brand line into two lines
  • Snapshot fund names β€” formatFundName_() was incorrectly applied at snapshot save time, truncating names (e.g. "Inuka Dozen Index Fund" β†’ "Inukadozen"). Now applied only at email render time; full names saved to sheets.

Added

  • ret_3m and ret_*_ear type fields added to EnrichedFundData interfaces in firestoreService.ts and fundDataService.ts

  • Email send logging (appscripts/lib.emailLog.gs) β€” logEmailBatch_() appends one row per send batch to the History spreadsheet: sent_emails for production, sent_as_test for test runs. Captures timestamp, type, report_id, subject, sent_count, failed_count, is_test. Wired into sendWeeklyPulseReport, testWeeklyReport, and sendCorrection_.

  • One-off correction email (appscripts/reports.correction.gs) β€” sendCorrectionEmail() / sendCorrectionEmailTest() sends a branded apology to subscribers explaining the EAR mislabeling, showing corrected figures for 2026-W16, and outlining what was fixed.


2026-04-18

Firebase Backend: Daily QA Report & Frontend Lint

Added

  • Daily QA Email Report (functions/src/reports.dailyEmail.ts) β€” Ports reports.dataQuality.gs from App Script to Firebase Cloud Functions. A new dailyQAReport scheduled function fires at 8am EAT, Mon–Fri and sends a rich HTML digest email covering: 24h pipeline run count, snapshots written, updated funds, stale funds (>3 days), and any collector errors. Email is sent via Gmail SMTP using secrets stored in Firebase Secret Manager (GMAIL_USER, GMAIL_APP_PASS).

  • Pipeline Run Logging β€” syncFundData now writes a pipeline_logs Firestore doc after every run ({timestamp, collected, persisted, errors[]}). This is the data source for the daily report, replacing the App Script QA sheet.

  • App Script β†’ Firebase Feature Audit (docs/appscript-vs-firebase-audit.md) β€” Comprehensive mapping of every App Script function against its Firebase equivalent, with status (ported / partial / not ported), rationale for gaps, and open questions.

  • Stage 3 Validation Script (scripts/validate-stage3.mjs) β€” CLI utility to compare live Firestore NAV data against the Apps Script timeSeries endpoint for all 16 funds. Used to verify migration correctness after backfills.

Fixed

  • Frontend ESLint β€” Resolved all 24 lint errors across 6 Vue components:
    • DailyPulse.vue β€” removed unused t and dead format() helper
    • Sidebar.vue β€” added defineOptions({ name: 'AppSidebar' }) for multi-word component rule
    • ComparisonMatrixView.vue β€” dropped unused Filter import; replaced any with Record<string, unknown> in sort, formatValue, and getRiskLevel
    • GoalsView.vue β€” removed unused computed, PinOff import, and formatDate
    • ReportSnapshotView.vue β€” removed unused getFundIcon function
    • WeeklyCheckupView.vue β€” removed unused ChevronRight and t; replaced catch (e: any) with unknown + instanceof Error guard

Infrastructure

  • Default shell set to Bash β€” Firebase deploys must be run from Git Bash (not PowerShell) due to PowerShell's heavier startup overhead exceeding the Firebase CLI's 10s local analysis timeout.
  • nodemailer lazy-loaded inside sendDailyQAReport (not at module init) to keep function cold-start time under the CLI analysis threshold.

Version History

v0.1.4 β€” Mobile Layout Fixes

Bug Fixes:

  • Subscribe Form β€” Name fields now stack vertically on mobile instead of being squeezed side-by-side
  • Weekly Reports Table β€” Responsive layout with hidden columns on mobile, dark mode support

v0.1.3 β€” Export Improvements

Improvements:

  • Responsive Export Footer β€” PNG exports now use a two-line footer on mobile devices:
    • Line 1: Title and subtitle centered
    • Line 2: NavViewTZ branding (left), timestamp and logo (right)
  • Prevents footer clutter on small screen exports

v0.1.2 β€” Mobile-First Optimization

New Features:

  • Scroll-to-Hide Hamburger β€” Menu button hides when scrolling down, reappears on scroll up (iOS-style)
  • Safe Area Padding β€” Proper bottom padding for iOS devices using env(safe-area-inset-bottom)

Improvements:

  • Mobile Layouts β€” Stacked layouts for Analysis, Calculator, and Compare views on small screens
  • Responsive Charts β€” Adaptive chart heights (300px mobile, 450px desktop)
  • View Controls β€” Icon-only mode on mobile with horizontal scroll
  • Footer Redesign β€” Links now stack vertically and center-align on mobile
  • Sidebar UX β€” Full text labels on mobile, X button repositioned to right, logo sizing fixed

Technical:

  • Reduced chart container min-heights for portrait orientation
  • Added responsive padding patterns (p-4 lg:p-6)
  • Improved PulseHero and PerformanceHeatmap header layouts

v0.1.1 β€” Calculator Enhancements

New Features:

  • Contribution Extrapolator β€” 3-band future projections (Optimistic, Neutral, Pessimistic)
  • Tabulated Investment Growth β€” Data tables with Past, Future, and Combined tabs
  • Modular Architecture β€” Compartmentalized Calculator into reusable components

Improvements:

  • PNG exports now capture full component (headers, cards, selectors, charts)
  • Added componentRef pattern for element-based exports

Bug Fixes:

  • Fixed watermark URL from navviewtz.vercel.app to nav-view-tz.vercel.app

v0.1.0 β€” Initial Release

Core Features:

  • Executive Dashboard with real-time KPIs
  • Momentum Pulse for top/bottom movers (1D/1W/1M/1Y)
  • Deep Analysis with historical performance charts
  • Return Comparator for side-by-side fund comparison
  • Investment Calculator for wealth simulation
  • Weekly Reports archive

Last updated: April 23, 2026

Built with ❀️ for Tanzania's investment community

Build: 113c70e