Changelog
Track the evolution of NavViewTZ
Have an idea?
Help shape the future of NavViewTZ
2026-08-28Comparator: Growth Metrics Matrix
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 β labelledSelected (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-28Sidebar Cleanup
Sidebar Cleanup
Removed
- Manual "Refresh Data" sidebar button (
src/components/Sidebar.vue) β redundant with the automatic refreshApp.vuealready triggers (fundsStore.refresh(true)); removed the button, itsisRefreshingspin state, and the now-unuseduseFundsStoreimport from the sidebar. The store's ownrefresh()method is untouched β only the manual UI trigger is gone.
2026-08-27WebMCP Surface and Firestore Fallbacks
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 intoonMounted. 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.icashwhile the Firestore document isitrust.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 topublic/data/history/duringprebuild, andfetchLatestfalls back to the existingpublic/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-idsinprebuild(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-testingin Chrome 151; without itdocument.modelContextis undefined and no tool registers. Tool behaviour is covered against an injected harness intests/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-22Monthly Digest: Fix Blank Pipeline Activity
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'scount()/sum()aggregate query overpipeline_logs(filtered bytimestamprange) was throwingFAILED_PRECONDITION: The query requires an indexon every run. The surroundingtry/catchswallowed 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
sectionErrorson 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," andtriggerMonthlyDigest's JSON response includes the real error β this is what made the missing index diagnosable in minutes instead of requiring log archaeology.
2026-08-12v0.4.1 MCP Guardrail Fixes from Live ChatGPT Evaluation
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 viaclient.getInstructions()) telling the model to always call a tool for current figures, resolvefund_ids fromget_market_briefing/search_fundsbefore callingcompare_funds, never callcompare_fundsmore 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 returnsstructuredContent: {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_fundsincompatible-comparison warning β both thestructuredContent.warningand 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) β strengthenedget_market_briefing,search_funds, andcompare_fundswith 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 chainingcompare_fundscalls across a single request, and an explicit no-recommendation instruction for thecomparable: falsecase.
2026-08-11v0.4.0 Public MCP Server for ChatGPT & Agents
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
navViewMcpFirebase 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 cachedpublic_exports/latest_fundsFirestore 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.mjsanddocs/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 outperformance_basis/headline_metricfor 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 Firestoremerge: truewrite actually preserved. AddedexistingHeadlineFields()to carry those fields forward explicitly, with unit coverage./mcprouting β the SPA's Edge Middleware (middleware.ts) was intercepting/mcpand returning its own 404 before thevercel.jsonrewrite to the Firebase function could run;/mcpis now excluded from the middleware's matcher.
2026-07-26Indexable Swahili Pages (closes #56)
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
/swlocale tree - every public page is mirrored at a/swprefix (/sw,/sw/compare,/sw/funds/utt-liquid,/sw/managers/itrust-finance). Routes are declared once and mirrored programmatically insrc/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,mapatofor NAV return andmazaofor published yield,kitengo,ada,ukwasi, NAV kept as-is. hreflangannotations - every page emits reciprocalen,sw, andx-defaultalternates server-side, andpublic/sitemap.xmlcarries the matchingxhtml:linkblocks (88 URLs across 2 locales).- Locale-aware regression checks -
check-seo-middleware.mjsnow asserts Swahili rendering,lang="sw", prefix-internal links, Swahili 404s, and hreflang reciprocity in both the responses and the sitemap.
Changed
- Locale resolution - a
/swURL now outrankslocalStorageand 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
structurefield, fixing the ungrammatical "as a Open-end Money Market" in English and giving Swahili a translated noun phrase.
2026-06-28v0.3.2 Phase 3 Technical Maturity Completed
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-28Optional GA4 Interaction Tracking (closes roadmap Β§3.5 Interaction Tracking)
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 exposestrackEvent()plus av-analyticsdirective 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_IDdeployment setting and the privacy boundary for tracked events.
All notable updates and features for NavViewTZ are documented here.
2026-06-21Reporting Overhaul: Monthly Digest, Source Uptime Metrics & Distinct Senders
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 fromrunIncidentCheck, so no extra pipeline work. Unit-tested insrc/__tests__/sourceHealth.test.ts. - Monthly Digest report (
functions/src/reports.monthlyDigest.ts) β NewmonthlyDigestReportscheduled 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 atriggerMonthlyDigestHTTPS 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) to0 7 * * *(7am, every day) so weekend NAVs are reported on. - Distinct email senders (
functions/src/config.tsMAIL_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-20In-App Feedback Link (closes roadmap Β§3.5 Feedback Mechanism)
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 exportsFEEDBACK_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 lucideMessageSquarePlusicon 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 inlineFEATURE_REQUEST_FORM_URLconstant to importFEEDBACK_FORM_URLfrom@/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-8togap-6withflex-wrapso the new entry lays out cleanly alongside Subscribe / WhatsApp / Methodology / About across the responsive breakpoints.
Verified
npm run buildpasses β no new warnings.
2026-06-20v0.3.1 Sticky Mobile Header & Managers Catalogue Polish
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 ausePageTitlecomposable 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
/managerspage 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.jsondefining 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-20to render beneath the sidebar menu.
2026-06-20Per-Fund Enrichment Dirty Flag (closes roadmap Β§3.3)
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 fortriggerEnrichmentandcleanAndRebackfillUTT).classifyFundForEnrichment(...)β pure helper exporting thefull | lightdecision 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
advancedFundsget 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_summarywith 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
syncFundDatapipeline (functions/src/index.ts) β when enrichment runs, passesadvancedFunds = new Set(persistedFunds)so only funds that actually persisted new rows trigger a full recompute. The outerhasAdvancedgate (skip enrichment entirely when zero funds advanced) is unchanged.enrichAllFundslog lines now report the full vs. light split each run, so steady-state reduction is visible in Cloud Logging without bespoke metrics./meta/fund_summarycorrectness 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 testinfunctions/β 115 / 115 tests pass (12 new partition tests, 14 existing calendar tests, 89 pre-existing).npm run buildinfunctions/β esbuild bundle compiles cleanly (157.2 kb).- Pre-existing
reports.weeklyPulse.ts:131typecheck failure (unrelatedArray.prototype.atlib target) is unchanged.
2026-06-20Tanzania Holiday Calendar for d1 Metrics
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_HOLIDAYSset (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
calcD1Returncanonical_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.tsupdated to reference the new helper and the gazetted calendar; the "future hook" wording removed. PERIOD_TOLERANCE_DAYSjustification 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 testinfunctions/β 103 / 103 tests pass (including the 14 new calendar tests).- Pre-existing typecheck failure in
reports.weeklyPulse.ts:131is unrelated to this change.
2026-06-20v0.3.0 Managers Catalogue, Weekly Reports Redesign & Sorting Controls
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 inpublic/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-18v0.2.2 Fund Pages & Discovery
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.txtandpublic/sitemap.xmlwith 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.txtandsitemap.xmlfrom the SPA catch-all rewrite so crawlers can fetch them directly.
2026-06-18Fund Logo Fallback & Layout Polish
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
gradientStyleinFundLogo.vueto generate high-quality, professional linear HSL gradients deterministically from the fund name or ID.
Changed
FundLogo.vuefallback hierarchy β Updated component logic:- Try loading specific fund logo (
/logos/<fundId>.png). - Fall back to manager logo (
/logos/<providerId>.png) with a small initials badge at the bottom-right. - Fall back to full-container initials with a modern gradient background if the manager logo is also missing.
- Try loading specific fund logo (
- Initials deduplication β Added a explicit initials lookup map (
FUND_INITIALS_MAP) to prevent identical initials for different funds (e.g.ZIfor 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-8like theh-5Select 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-16Orbit Collector: Drop Livewire, Use Static Panels
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 itsdata-history-panel="<name>"marker up to the nextdata-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 theCOLLECTION FAILEDalert, so the layout canary will fire when a panel reappears with a different structure. spot-check.mjsβ Orbit checks now probe for the twodata-history-panelmarkers 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 FAILEDalert contract it introduced is preserved on the missing-panel branches.
2026-06-15Source-Change Monitoring & Orbit Collector Hardening
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 immediateSchemaAlert, 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 explicitCOLLECTION FAILEDalert is raised when no table can be found. Whatever recognised columns are present (at minimumDATE+Net Asset Value) are ingested rather than dropping the fund entirely.CollectorResultβ Gained an optionalfingerprintsmap (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
persistedFundstracking in pipeline logs β ThesyncFundDatapipeline 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
persistedFundsdata 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 leveragepersistedFundsfor the "Funds Updated" table in the daily email digest.
2026-04-30Functions Audit & Maintenance Documentation
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/.
- Fetches pipeline run success rates from Firestore
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-23Scraper Reliability Fixes: fetchWithRetry & Orbit CSRF
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 infetchWithRetry(Critical, affects ALL scrapers)The
AbortControllerand 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+setTimeoutper attempt, andclearTimeoutin both success and error paths.Before After 1 AbortController for all 4 attempts 1 AbortController per attempt Timeout fires once, kills all retries Timeout scoped to each attempt Timer leaked on catch Timer cleared on catch collect.orbit.tsβ CSRF token never sent to Laravel (Orbit-only, caused HTTP 419)buildHeaders()setX-Livewire,Content-Type,User-Agent,Referer, andCookieβ but never setX-CSRF-Token. ThenlivewirePost()tried to readheaders["X-CSRF-Token"]for the request body_tokenfield, which was alwaysundefined, falling back toextractTokenFromSnapshot()which returned the Livewire snapshot checksum β not the Laravel CSRF token. Laravel correctly rejected every POST with HTTP 419 "Page Expired".Fix:
buildHeaders()now includes"X-CSRF-Token": csrfin the returned headers objectlivewirePost()now receivescsrfas a direct parameter and uses it for the body_tokenfield, 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.tzandorbit.co.tzresponding 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-23Orbit Collector Fix, Backfill & Gap-Aware Staleness
Orbit Collector Fix, Backfill & Gap-Aware Staleness
Fixed
collect.orbit.tsbroken Livewire component β Orbit split their page into two components:net-asset-value(live summary) andhistorical-net-asset-value(historical table). The collector was callingtoggleTableon the wrong component, causing HTTP 500 since at least Apr 21. Fixed by targetinghistorical-net-asset-valuefor the Livewire interaction. Collector is working again β no data loss on existing Firestore data.spot-check.mjssmoke check β updated to verifyhistorical-net-asset-valueinstead ofnet-asset-value, so a future component rename will be caught at deploy time.collect.orbit.test.tsfixtures β 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 fororbit.inukaand 31 fororbit.inukaDozen(AppScript-era single-row scraping holes and the recent Apr 21β22 collector outage).Gap-aware staleness in
staleness.tsβgetStalenessMapnow scans the lastPIPELINE.GAP_SCAN_ROWS(60) snapshots per fund instead of just the latest one. If any consecutive gap exceedsPIPELINE.GAP_THRESHOLD_DAYS(5 days),daysStaleis bumped to cover back to the gap edge. This feedsderiveFetchLimitwhich then returns a deep enough window to fill the gap on the next collector run β self-healing without manual intervention.PIPELINE.GAP_SCAN_ROWS: 60added toconfig.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-23Stage 3 Migration Complete & Monthly Coverage Check
Stage 3 Migration Complete & Monthly Coverage Check
Migration: Stage 3 Complete
scripts/validate-stage3.mjsβ fixed a field name mismatch: the validator was readingreturn_1m,return_3m,sparklinefrom Firestore but the enrichment function writesret_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 withlimit(35); no new Cloud Function required.
2026-04-23Firestore Audit Expanded to All 16 Funds
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 viaPromise.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.wekezaChange Detail Per-fund stale threshold WHI gets 7 days (government fund); all others 3 days Parallel Firestore reads count(), recent snapshots, and oldest-date queries run concurrently per fundExit code 1on any hard error (missing fund doc / empty subcollection);0on warnings-only or cleanSummary table Printed 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-23Live Smoke Pre-Deploy Gate
Live Smoke Pre-Deploy Gate
Added
scripts/spot-check.mjsexpanded to all 5 data sources β previously only covered iTrust and Zansec. Now verifies every upstream source before each deploy:Source Check iTrust (JSON API) All 6 expected fields present; dates parse as MM/DD/YYYY; core numeric fields non-nullZansec (HTML table) table#datatablepresent; all 6 expected column headers match; latest 3 rows parse cleanlyWHI (HTML table) table#example1present; 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; /navsAPI returns rows; at least onesnamemaps to a known scheme; latest date parses asDD-MM-YYYYOrbit (Livewire) Page loads; data-csrfattribute present;wire:snapshotfor thenet-asset-valuecomponent is presentExit code gate β script now exits
1on 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-22Fixture-Based Unit Tests for All Collectors
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.File Tests Coverage schemaCheck.test.ts15 All 4 pure check functions β valid data returns null, renamed/missing columns return correct alerts collect.iTrust.test.ts6 API field mapping, MM/DD/YYYY β YYYY-MM-DD parsing, fetch-depth limiting, schema alert on field rename collect.htmlTable.test.ts10 WHI + Zansec parsing from live fixture HTML, staleness-driven row depth, schema alert on column rename collect.orbit.test.ts5 Livewire 3-step flow with stubbed responses, DD-MM-YYYY parsing, schema alert on column rename collect.uttamis.test.ts6 Scheme 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. Runnode scripts/fetch-fixtures.mjsto refresh the WHI and Zansec fixtures when source sites change; Orbit is hand-crafted from the expected#historicalNAVTablestructure.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 passfunctions/tsconfig.jsonβ Excludessrc/__tests__sotsc --noEmitdoesn't applynoUnusedLocalsto test filesfunctions/vitest.config.tsβ Minimal Vitest config targetingsrc/__tests__/**/*.test.ts
2026-04-22Schema Change Alerting & Pipeline Diagnostics
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 structuredSchemaAlertobjects (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
#historicalNAVTablecolumn headers after Livewire interaction (checkOrbitHeaders) - UTT AMIS β detects unrecognised scheme names, meaning a new or renamed UTT fund has appeared (
checkUttSchemeNames)
- iTrust β validates JSON field names in API response (
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 Detectedblock when alerts were logged in the last 24 h. Schema alerts also flip the overall status indicator from green to red.schemaAlertsfield in pipeline logs β Each/pipeline_logs/{id}document now stores aschemaAlertsarray alongsideerrors, 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-endfirestore-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 returnCollectorResult { snapshots, schemaAlerts }instead of plainRawSnapshot[] syncFundDatanow declaresGMAIL_USERandGMAIL_APP_PASSsecrets so it can send immediate schema alerts without depending on thedailyQAReportfunction
2026-04-22Android Widget Support & Publish Lag Metrics
Android Widget Support & Publish Lag Metrics
Added
Widget static metadata (
functions/src/index.ts) βseedFundMetadatanow 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 nativenumber[]array of the last 7 daily NAV values, oldest-first. Written to both/funds/{id}and/enriched/{date}. Distinct fromsparkline_1y(50-point yearly overview stored as JSON string) β the 7-day version is a native Firestore array for direct FlutterList<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'scollected_at, stored in hours (float):latest_publish_lagβ lag for the most recent snapshotavg_publish_lag_7dβ rolling mean of last 7 valid snapshotsavg_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 validcollected_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.mdin 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 completeFundWidgetModel.fromFirestore()factory.
Changed
seedFundMetadatamanager 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_7dmissing from enriched archive write βsparkline_7dwas computed and written to the top-level fund doc in commitb7aab9bbut omitted from the/enriched/{date}subcollection write. Fixed in3c03467.
Unreleased
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; useSTALEto show "β based on NAV 6 days apart β holiday period" /methodologypage linking todocs/metrics-methodology.md- Comparison matrix updated to use
ret_*_earfields explicitly
2026-04-20Functional Design Token Architecture v0.2.0
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
:rootconfig. - Fixed Semantic Mismatches β Resolved structural issues where components used incorrect variables, ensuring the design system flows correctly from the root.
2026-04-19NAV Lookup Methodology Overhaul & Enriched History
NAV Lookup Methodology Overhaul & Enriched History
Changed
d1 Previous Trading Day Strategy (
enrich.performanceMetrics.ts/js) β d1 no longer targetsago(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=3tolerance applied to all periods with a documented per-period configPERIOD_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.STALEconfidence level β no longer hard-refusing calculations beyond tolerance. NAVs found beyondPERIOD_TOLERANCE_DAYSbut within a 30-day hard cap are calculated and flaggedSTALErather than discarded. Confidence ladder is nowEXACT β APPROXIMATE β STALE β INSUFFICIENT.prevCanonicalWorkingDayβ new helper that returns the last MonβFri calendar day, used as the d1 canonical target for documentation andmetrics_meta. Explicitly marked as the future hook forprevPublicTanzaniaWorkingDayonce a Tanzania public holiday calendar is available.metrics_metaschema extended β d1 now includescanonical_start(the prevCanonicalWorkingDay result) alongsidestart,end,days,nav_s,nav_e,conf. Frontend can use the gap betweencanonical_startandstartto 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 ontransformAndPersistreturning new data.Both
enrich.performanceMetrics.ts(Cloud Functions) andenrich.performanceMetrics.js(Apps Script) kept in full sync throughout.
Added
Enriched metrics history subcollection β
enrichAllFundsnow 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_attimestamp. Static fund metadata excluded (lives on the parent doc). Enables metric trend tracking, retroactive NAV audit, and future historical views.EnrichedMetricsSnapshottype β TypeScript interface for the historical enriched record.Firestore indexes (
firestore.indexes.json) β two composite indexes for the newenrichedsubcollection: per-fund date-descending, and cross-fund byconsensus_date+freshness_status.
2026-04-18Build Fixes & Vercel Deployment
Build Fixes & Vercel Deployment
Fixed
getPeriodExplanationmissing export (src/utils/interestRates.ts) β function was imported byDevInterestRatesView.vuebut never exported; added implementation mapping eachPeriodto a human-readable description.- Firebase package corruption β
node_modules/firebase/directory existed without apackage.json, causing TypeScript to fail onfirebase/firestoreandfirebase/authsubpath imports. Resolved via clean reinstall. DevInterestRatesView.vuestale type references β view was using removed/renamedReturnResultproperties (isValid,dataPoints,latestDate,referenceDate); updated to current fields (value !== null,actualDays,endDate,startDate). Also typedperiodLabelsasPartial<Record<Period, string>>since the 3m period is intentionally excluded from this view.firestoreService.tsnon-null narrowing βsnapshotSnap.docs[0]flagged as possiblyundefineddespite!emptyguard; 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-18Premium Branding & Logo Refinement
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.pngand 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.htmlmeta 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.vuecomponent in favor of the new branded raster assets.
2026-04-18Dual-Track Returns, Email Corrections & Infrastructure
Dual-Track Returns, Email Corrections & Infrastructure
Fixed
- EAR mislabeling bug β
ret_1w(and allret_*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/1ynow store simple returns (actual period gain). Six newret_*_earfields 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 toret_*_earfields since it displays all periods side-by-side. Column headers now explicitly labeledp.a.All other components (LatestFundData, DailyPulse, DeepInsightCards, PulseHero, WatchlistWidget) continue using simpleret_*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_3mandret_*_eartype fields added toEnrichedFundDatainterfaces infirestoreService.tsandfundDataService.tsEmail send logging (
appscripts/lib.emailLog.gs) βlogEmailBatch_()appends one row per send batch to the History spreadsheet:sent_emailsfor production,sent_as_testfor test runs. Captures timestamp, type, report_id, subject, sent_count, failed_count, is_test. Wired intosendWeeklyPulseReport,testWeeklyReport, andsendCorrection_.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-18Firebase Backend: Daily QA Report & Frontend Lint
Firebase Backend: Daily QA Report & Frontend Lint
Added
Daily QA Email Report (
functions/src/reports.dailyEmail.ts) β Portsreports.dataQuality.gsfrom App Script to Firebase Cloud Functions. A newdailyQAReportscheduled 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 β
syncFundDatanow writes apipeline_logsFirestore 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 unusedtand deadformat()helperSidebar.vueβ addeddefineOptions({ name: 'AppSidebar' })for multi-word component ruleComparisonMatrixView.vueβ dropped unusedFilterimport; replacedanywithRecord<string, unknown>in sort,formatValue, andgetRiskLevelGoalsView.vueβ removed unusedcomputed,PinOffimport, andformatDateReportSnapshotView.vueβ removed unusedgetFundIconfunctionWeeklyCheckupView.vueβ removed unusedChevronRightandt; replacedcatch (e: any)withunknown+instanceof Errorguard
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.
nodemailerlazy-loaded insidesendDailyQAReport(not at module init) to keep function cold-start time under the CLI analysis threshold.
Version History
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
componentRefpattern for element-based exports
Bug Fixes:
- Fixed watermark URL from
navviewtz.vercel.apptonav-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