diff --git a/apps/finance/services/api/main/handler.go b/apps/finance/services/api/main/handler.go index 5fc7281..b1fa4b2 100644 --- a/apps/finance/services/api/main/handler.go +++ b/apps/finance/services/api/main/handler.go @@ -223,7 +223,8 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { for _, c := range cats { catNames[c.Name] = c.Name catColors[c.Name] = c.Color - if c.BudgetCents > 0 { + // exclude fixed categories from budget health — they're committed costs, not variable spend + if c.BudgetCents > 0 && !FixedCategories[c.Name] { catBudgets[c.Name] = c.BudgetCents } } @@ -379,15 +380,30 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { lastMonthSavingsRatePct = int(float64(lastMonthSavings) / float64(lastMonthIncome) * 100) } - // portfolio snapshot (best-effort, ignore errors) + // portfolio snapshot — degrade to cost basis if live prices are unavailable var portfolioValueCents, portfolioPCLCents int64 var portfolioHoldings []Holding + var portfolioPricesAvailable bool if trades, err := h.store.getTrades(ctx, a.UserID); err == nil && len(trades) > 0 { - if prices, err := fetchPricesByISIN(uniqueISINs(trades)); err == nil { - pr := aggregatePortfolio(computeHoldings(trades, prices)) + prices, _ := fetchPricesByISIN(uniqueISINs(trades)) + holdings := computeHoldings(trades, prices) + pr := aggregatePortfolio(holdings) + portfolioHoldings = pr.Holdings + + // check whether any prices came back + for _, p := range prices { + if p > 0 { + portfolioPricesAvailable = true + break + } + } + + if portfolioPricesAvailable { portfolioValueCents = pr.TotalVal portfolioPCLCents = pr.TotalPCL - portfolioHoldings = pr.Holdings + } else { + // fall back to cost basis so the card is still useful + portfolioValueCents = pr.TotalCost } } @@ -414,9 +430,10 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) { SafetyBufferCents: safetyBuffer, SavingsRatePct: savingsRatePct, LastMonthSavingsRatePct: lastMonthSavingsRatePct, - PortfolioValueCents: portfolioValueCents, - PortfolioPCLCents: portfolioPCLCents, - PortfolioHoldings: portfolioHoldings, + PortfolioValueCents: portfolioValueCents, + PortfolioPCLCents: portfolioPCLCents, + PortfolioHoldings: portfolioHoldings, + PortfolioPricesAvailable: portfolioPricesAvailable, }) } diff --git a/apps/finance/services/api/main/models.go b/apps/finance/services/api/main/models.go index 49a7357..d7bd27d 100644 --- a/apps/finance/services/api/main/models.go +++ b/apps/finance/services/api/main/models.go @@ -146,9 +146,10 @@ type DashboardData struct { SavingsRatePct int // savings / income * 100 this month LastMonthSavingsRatePct int - PortfolioValueCents int64 - PortfolioPCLCents int64 - PortfolioHoldings []Holding + PortfolioValueCents int64 + PortfolioPCLCents int64 + PortfolioHoldings []Holding + PortfolioPricesAvailable bool } type PeriodSummary struct { diff --git a/apps/finance/services/api/main/templates/dashboard.html b/apps/finance/services/api/main/templates/dashboard.html index bb2c43f..02b62a7 100644 --- a/apps/finance/services/api/main/templates/dashboard.html +++ b/apps/finance/services/api/main/templates/dashboard.html @@ -57,12 +57,16 @@

Portfolio today

- {{if $d.PortfolioValueCents}} + {{if $d.PortfolioHoldings}}
€0.00
+ {{if $d.PortfolioPricesAvailable}}

{{if ge $d.PortfolioPCLCents 0}}+{{else}}−{{end}}€{{cents (centsAbs $d.PortfolioPCLCents)}} total P&L

{{else}} +

cost basis · prices unavailable

+ {{end}} + {{else}}
No trades yet

Import trades →

{{end}} @@ -120,19 +124,24 @@
{{.Name}}
-
{{.ISIN}} · {{printf "%.2f" .SharesOwned}} shares
+
{{printf "%.4f" .SharesOwned}} shares
+ {{if $d.PortfolioPricesAvailable}}
€{{cents .CurrentValueCents}}
{{pctSign .UnrealizedPCLPct}}{{printf "%.1f" .UnrealizedPCLPct}}%
+ {{else}} +
€{{cents .TotalCostCents}}
+
cost basis
+ {{end}}
{{end}}
- Total - Total{{if not $d.PortfolioPricesAvailable}} invested{{end}} + €0.00