diff --git a/backend/app/core/config.py b/backend/app/core/config.py index fb8f56e..50823fb 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -7,7 +7,7 @@ from pydantic_settings import BaseSettings, SettingsConfigDict # title, /api/health (which the frontend reads to show it in Settings), and # embedded in exported backups so an old file can be told apart from a # current one. -APP_VERSION = "1.1.4" +APP_VERSION = "1.1.5" class Settings(BaseSettings): diff --git a/frontend/package.json b/frontend/package.json index 161e9bf..6018dad 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "aurum-frontend", "private": true, - "version": "1.1.4", + "version": "1.1.5", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/components/crypto/CryptoAllocationBody.tsx b/frontend/src/components/crypto/CryptoAllocationBody.tsx index 73ff61e..26fa716 100644 --- a/frontend/src/components/crypto/CryptoAllocationBody.tsx +++ b/frontend/src/components/crypto/CryptoAllocationBody.tsx @@ -1,5 +1,5 @@ import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; -import { formatCurrency, maskAmount } from "@/lib/format"; +import { formatCryptoAmount, maskAmount } from "@/lib/format"; import { useTranslation } from "@/lib/i18n"; import type { CryptoHolding } from "@/types"; @@ -65,7 +65,7 @@ function DonutTooltip({

{slice.key === "other" ? otherLabel : slice.name}

- {maskAmount(formatCurrency(slice.amount), hidden)} · {slice.percent.toFixed(1)}% + {maskAmount(formatCryptoAmount(slice.amount), hidden)} · {slice.percent.toFixed(1)}%

); @@ -132,7 +132,7 @@ export function CryptoAllocationBody({ holdings, isLoading, hidden }: CryptoAllo {slice.percent.toFixed(1)}% - {maskAmount(formatCurrency(slice.amount), hidden)} + {maskAmount(formatCryptoAmount(slice.amount), hidden)} ))} diff --git a/frontend/src/components/crypto/CryptoHoldingsTable.tsx b/frontend/src/components/crypto/CryptoHoldingsTable.tsx index 0c84557..cebb983 100644 --- a/frontend/src/components/crypto/CryptoHoldingsTable.tsx +++ b/frontend/src/components/crypto/CryptoHoldingsTable.tsx @@ -1,5 +1,5 @@ import { Bitcoin, Plus, Trash2 } from "lucide-react"; -import { formatCurrency, maskAmount } from "@/lib/format"; +import { formatCryptoAmount, maskAmount } from "@/lib/format"; import { useTranslation } from "@/lib/i18n"; import type { CryptoHolding, CryptoPortfolio } from "@/types"; @@ -106,7 +106,7 @@ export function CryptoHoldingsTable({ {holding.current_price !== null - ? maskAmount(formatCurrency(holding.current_price), hidden) + ? maskAmount(formatCryptoAmount(holding.current_price), hidden) : t("crypto.pendingPrice")} @@ -120,20 +120,20 @@ export function CryptoHoldingsTable({ - {holding.value !== null ? maskAmount(formatCurrency(holding.value), hidden) : t("crypto.pendingPrice")} + {holding.value !== null ? maskAmount(formatCryptoAmount(holding.value), hidden) : t("crypto.pendingPrice")} {maskAmount(`${Number(holding.quantity)} ${holding.symbol}`, hidden)} - {holding.avg_buy_price !== null ? maskAmount(formatCurrency(holding.avg_buy_price), hidden) : "—"} + {holding.avg_buy_price !== null ? maskAmount(formatCryptoAmount(holding.avg_buy_price), hidden) : "—"} {holding.profit_loss !== null && holding.profit_loss_percent !== null ? ( <> - {maskAmount(`${sign}${formatCurrency(holding.profit_loss)}`, hidden)} + {maskAmount(`${sign}${formatCryptoAmount(holding.profit_loss)}`, hidden)} {sign} diff --git a/frontend/src/components/crypto/CryptoTransactionHistoryModal.tsx b/frontend/src/components/crypto/CryptoTransactionHistoryModal.tsx index 3158281..793bacf 100644 --- a/frontend/src/components/crypto/CryptoTransactionHistoryModal.tsx +++ b/frontend/src/components/crypto/CryptoTransactionHistoryModal.tsx @@ -1,7 +1,7 @@ import { Pencil, Trash2 } from "lucide-react"; import { Dialog } from "@/components/ui/Dialog"; import { useCryptoTransactions, useDeleteCryptoTransaction } from "@/hooks/useCrypto"; -import { formatCurrency, formatTransactionDate, maskAmount } from "@/lib/format"; +import { formatCryptoAmount, formatTransactionDate, maskAmount } from "@/lib/format"; import { useTranslation } from "@/lib/i18n"; import type { CryptoHolding, CryptoTransaction } from "@/types"; @@ -47,7 +47,7 @@ export function CryptoTransactionHistoryModal({ open, onClose, holding, hidden, - {maskAmount(`${Number(tx.quantity)} ${holding.symbol} · ${formatCurrency(tx.price_per_unit)}`, hidden)} + {maskAmount(`${Number(tx.quantity)} ${holding.symbol} · ${formatCryptoAmount(tx.price_per_unit)}`, hidden)} {formatTransactionDate(tx.date, true)} diff --git a/frontend/src/lib/format.ts b/frontend/src/lib/format.ts index 5db7013..ed9035e 100644 --- a/frontend/src/lib/format.ts +++ b/frontend/src/lib/format.ts @@ -17,6 +17,29 @@ export function formatCurrency(amount: number | string, currency: string = getCu }).format(value); } +/** Same currency formatting as formatCurrency, but scales decimal precision + * down to the value's own magnitude instead of always rounding to whole + * units — a low-cap memecoin can genuinely price at $0.000000006894, and + * formatCurrency's fixed 0 fraction digits would render that as "$0", + * indistinguishable from actually being worthless. Values >= 1 still show + * just 2 decimals (a coin price doesn't need more than cents once it's + * above a dollar). For the Crypto tab's per-coin price/holdings/avg-buy-price + * cells and per-transaction price — anywhere a single coin's own value + * needs to be told apart from zero, not just a portfolio-wide total. */ +export function formatCryptoAmount(amount: number | string, currency: string = getCurrency()): string { + const value = typeof amount === "string" ? Number(amount) : amount; + const abs = Math.abs(value); + const maximumFractionDigits = + abs === 0 || abs >= 1 + ? 2 + : // Leading zeros right after the decimal point before the first + // significant digit, plus 4 more digits of real precision beyond + // that — e.g. 0.000000006894 has 8 leading zeros, so this shows + // 12 decimal places, landing exactly on "6894" and nothing more. + Math.min(20, Math.max(0, -Math.floor(Math.log10(abs)) - 1) + 4); + return new Intl.NumberFormat(getIntlLocale(), { style: "currency", currency, maximumFractionDigits }).format(value); +} + export function formatSignedCurrency(amount: number | string, currency: string = getCurrency()): string { const value = typeof amount === "string" ? Number(amount) : amount; const sign = value > 0 ? "+" : "";