From 5d4e87e69beb5173e49c4d6bb29b6325d1cb992a Mon Sep 17 00:00:00 2001 From: Jonathan Kirkwood Date: Tue, 14 Jul 2026 09:10:14 +0200 Subject: [PATCH] 0.2.40: bitcoin-denominated line on the LP capital chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The capital-over-time chart gains a dashed orange series on its own right-hand ₿ axis: total value (ending balance + distributions) divided by the BTC price at each statement date — the time-series of the "In bitcoin terms" box. The history table gains a matching In-bitcoin column. Frontend-only; prices were already stamped per statement. Co-Authored-By: Claude Fable 5 --- deploy/package.json | 2 +- deploy/startos/install/versions/index.ts | 5 +- deploy/startos/install/versions/v_0_2_40.ts | 13 +++++ frontend/public/sw.js | 2 +- frontend/src/components/CapitalChart.tsx | 61 ++++++++++++++++++++- frontend/src/portal/InvestorPortalView.tsx | 14 +++++ frontend/src/version.ts | 2 +- 7 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 deploy/startos/install/versions/v_0_2_40.ts diff --git a/deploy/package.json b/deploy/package.json index 8ff9ce2..4037f38 100644 --- a/deploy/package.json +++ b/deploy/package.json @@ -1,6 +1,6 @@ { "name": "ten31portal-startos", - "version": "0.2.39", + "version": "0.2.40", "private": true, "scripts": { "build": "npm run check && rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", diff --git a/deploy/startos/install/versions/index.ts b/deploy/startos/install/versions/index.ts index afd4191..87ca3d9 100644 --- a/deploy/startos/install/versions/index.ts +++ b/deploy/startos/install/versions/index.ts @@ -1,4 +1,4 @@ -export { v_0_2_39 as current } from './v_0_2_39' +export { v_0_2_40 as current } from './v_0_2_40' import { v_0_1_0 } from './v_0_1_0' import { v_0_2_0 } from './v_0_2_0' import { v_0_2_1 } from './v_0_2_1' @@ -38,4 +38,5 @@ import { v_0_2_35 } from './v_0_2_35' import { v_0_2_36 } from './v_0_2_36' import { v_0_2_37 } from './v_0_2_37' import { v_0_2_38 } from './v_0_2_38' -export const other = [v_0_1_0, v_0_2_0, v_0_2_1, v_0_2_3, v_0_2_4, v_0_2_5, v_0_2_6, v_0_2_7, v_0_2_8, v_0_2_9, v_0_2_10, v_0_2_11, v_0_2_12, v_0_2_13, v_0_2_14, v_0_2_15, v_0_2_16, v_0_2_17, v_0_2_18, v_0_2_19, v_0_2_20, v_0_2_21, v_0_2_22, v_0_2_23, v_0_2_24, v_0_2_25, v_0_2_26, v_0_2_27, v_0_2_28, v_0_2_29, v_0_2_30, v_0_2_31, v_0_2_32, v_0_2_33, v_0_2_34, v_0_2_35, v_0_2_36, v_0_2_37, v_0_2_38] +import { v_0_2_39 } from './v_0_2_39' +export const other = [v_0_1_0, v_0_2_0, v_0_2_1, v_0_2_3, v_0_2_4, v_0_2_5, v_0_2_6, v_0_2_7, v_0_2_8, v_0_2_9, v_0_2_10, v_0_2_11, v_0_2_12, v_0_2_13, v_0_2_14, v_0_2_15, v_0_2_16, v_0_2_17, v_0_2_18, v_0_2_19, v_0_2_20, v_0_2_21, v_0_2_22, v_0_2_23, v_0_2_24, v_0_2_25, v_0_2_26, v_0_2_27, v_0_2_28, v_0_2_29, v_0_2_30, v_0_2_31, v_0_2_32, v_0_2_33, v_0_2_34, v_0_2_35, v_0_2_36, v_0_2_37, v_0_2_38, v_0_2_39] diff --git a/deploy/startos/install/versions/v_0_2_40.ts b/deploy/startos/install/versions/v_0_2_40.ts new file mode 100644 index 0000000..75b9dca --- /dev/null +++ b/deploy/startos/install/versions/v_0_2_40.ts @@ -0,0 +1,13 @@ +import { VersionInfo } from '@start9labs/start-sdk' + +export const v_0_2_40 = VersionInfo.of({ + version: '0.2.40:0', + releaseNotes: { + en_US: + 'The bitcoin-denominated value now plots on the LP capital chart: a dashed orange line on its own right-hand ₿ axis shows total value (capital balance + distributions) in bitcoin at each statement date, alongside the dollar series. The statement history table gains a matching "In bitcoin" column. Appears automatically wherever BTC prices are uploaded; no setup change.', + }, + migrations: { + up: async ({ effects }) => {}, + down: async ({ effects }) => {}, + }, +}) diff --git a/frontend/public/sw.js b/frontend/public/sw.js index aac4661..aec2881 100644 --- a/frontend/public/sw.js +++ b/frontend/public/sw.js @@ -3,7 +3,7 @@ // - content-hashed /assets/* are cache-first (immutable, safe forever) // - /api/* is never cached // Bump CACHE on each release so old entries are purged. -const CACHE = 'ten31-portal-0.2.39' +const CACHE = 'ten31-portal-0.2.40' self.addEventListener('install', () => self.skipWaiting()) diff --git a/frontend/src/components/CapitalChart.tsx b/frontend/src/components/CapitalChart.tsx index 8f3099e..5263e38 100644 --- a/frontend/src/components/CapitalChart.tsx +++ b/frontend/src/components/CapitalChart.tsx @@ -6,6 +6,7 @@ export interface CapitalPoint { value: number; // ending balance, cents paidIn: number; // cumulative contributions, cents distributions: number; // cumulative distributions, cents + btcValue?: number | null; // total value (NAV + distributions) in BTC at that date's price } const SERIES = [ @@ -14,6 +15,13 @@ const SERIES = [ { key: "distributions" as const, label: "Distributions", color: "#16a34a" }, ]; +const BTC_COLOR = "#f7931a"; + +const formatBtcAxis = (v: number) => { + const dp = v >= 10 ? 1 : v >= 1 ? 2 : 3; + return `₿${v.toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp })}`; +}; + // A compact, dependency-free multi-line SVG chart of an investor's capital over time. export default function CapitalChart({ points }: { points: CapitalPoint[] }) { const data = useMemo( @@ -28,10 +36,18 @@ export default function CapitalChart({ points }: { points: CapitalPoint[] }) { const hasDistributions = data.some((d) => d.distributions > 0); const series = hasDistributions ? SERIES : SERIES.filter((s) => s.key !== "distributions"); + // The bitcoin-denominated value plots on its own right-hand axis — BTC and dollar + // magnitudes are incomparable. Prices may be missing for the earliest statements + // (uploaded CSV starts later), so the ₿ line covers only the dated points it has. + const btcPoints = data + .map((d, i) => ({ i, btc: d.btcValue })) + .filter((p): p is { i: number; btc: number } => p.btc != null && p.btc > 0); + const showBtc = btcPoints.length >= 2; + const W = 640; const H = 240; const padL = 64; - const padR = 16; + const padR = showBtc ? 60 : 16; const padT = 16; const padB = 32; const innerW = W - padL - padR; @@ -41,6 +57,9 @@ export default function CapitalChart({ points }: { points: CapitalPoint[] }) { const x = (i: number) => padL + (data.length === 1 ? innerW / 2 : (innerW * i) / (data.length - 1)); const y = (v: number) => padT + innerH - (innerH * v) / maxVal; + const maxBtc = showBtc ? Math.max(...btcPoints.map((p) => p.btc)) : 1; + const yBtc = (v: number) => padT + innerH - (innerH * v) / maxBtc; + // 4 horizontal gridlines with dollar labels. const ticks = [0, 0.25, 0.5, 0.75, 1].map((f) => Math.round(maxVal * f)); @@ -56,6 +75,21 @@ export default function CapitalChart({ points }: { points: CapitalPoint[] }) { ))} + {showBtc && + [0.25, 0.5, 0.75, 1].map((f) => ( + + {formatBtcAxis(maxBtc * f)} + + ))} + {series.map((s) => ( `${x(p.i)},${yBtc(p.btc)}`).join(" ")} + /> + )} + {showBtc && + btcPoints.map((p) => ( + + {`${formatQuarter(data[p.i].date)} · In bitcoin: ${formatBtcAxis(p.btc)}`} + + ))} + {data.map((d, i) => ( {formatQuarter(d.date)} @@ -88,6 +138,15 @@ export default function CapitalChart({ points }: { points: CapitalPoint[] }) { {s.label} ))} + {showBtc && ( + + + In bitcoin (right axis) + + )} ); diff --git a/frontend/src/portal/InvestorPortalView.tsx b/frontend/src/portal/InvestorPortalView.tsx index c9a4a8b..c2b9eb1 100644 --- a/frontend/src/portal/InvestorPortalView.tsx +++ b/frontend/src/portal/InvestorPortalView.tsx @@ -471,12 +471,18 @@ function HistorySection({ history }: { history: CapitalAccount[] }) { const [showChart, setShowChart] = useState(false); if (history.length < 2) return null; + // Total value (NAV + distributions received) in BTC at each statement date's price — + // the time-series of the "In bitcoin terms" box. Null until prices cover that date. const chartPoints: CapitalPoint[] = history.map((a) => ({ date: a.as_of_date, value: a.ending_balance_cents, paidIn: a.contributions_cents, distributions: a.distributions_cents, + btcValue: a.btc_price_cents + ? (a.ending_balance_cents + a.distributions_cents) / a.btc_price_cents + : null, })); + const hasBtc = history.some((a) => a.btc_price_cents); return (
@@ -502,6 +508,7 @@ function HistorySection({ history }: { history: CapitalAccount[] }) { Paid-in Distributions Ending balance + {hasBtc && In bitcoin} @@ -511,6 +518,13 @@ function HistorySection({ history }: { history: CapitalAccount[] }) { {formatMoneyExact(a.contributions_cents)} {formatMoneyExact(a.distributions_cents)} {formatMoneyExact(a.ending_balance_cents)} + {hasBtc && ( + + {a.btc_price_cents + ? formatBtc(a.ending_balance_cents + a.distributions_cents, a.btc_price_cents) + : "—"} + + )} ))} diff --git a/frontend/src/version.ts b/frontend/src/version.ts index 6ba8fcd..0940ed9 100644 --- a/frontend/src/version.ts +++ b/frontend/src/version.ts @@ -1,4 +1,4 @@ // Bumped each release so the running build is visible in the UI. // If the number shown in the app doesn't match the installed s9pk version, // the new frontend isn't actually being served. -export const APP_VERSION = "0.2.39"; +export const APP_VERSION = "0.2.40";