0.2.35: exited positions show history clipped at the exit date

The eNAV keeps producing statements after an exit (quarterly for funds,
event-driven for SPVs), so an unfiltered graph would crash to zero.
Now the exited card gets its collapsible History back — chart + table
showing the capital journey up to exited_on only. Post-exit statements
stay recorded (audit trail; Undo restores full history) but are never
plotted. History section extracted into a shared HistorySection
component; toggle counts "statements" not "quarters" (SPV statements
are event-driven).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-03 09:23:47 -05:00
co-authored by Claude Opus 4.8
parent 6313d781b4
commit 2c87ab499a
6 changed files with 59 additions and 32 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ten31portal-startos", "name": "ten31portal-startos",
"version": "0.2.34", "version": "0.2.35",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "npm run check && rm -rf ./javascript && ncc build startos/index.ts -o ./javascript", "build": "npm run check && rm -rf ./javascript && ncc build startos/index.ts -o ./javascript",
+3 -2
View File
@@ -1,4 +1,4 @@
export { v_0_2_34 as current } from './v_0_2_34' export { v_0_2_35 as current } from './v_0_2_35'
import { v_0_1_0 } from './v_0_1_0' import { v_0_1_0 } from './v_0_1_0'
import { v_0_2_0 } from './v_0_2_0' import { v_0_2_0 } from './v_0_2_0'
import { v_0_2_1 } from './v_0_2_1' import { v_0_2_1 } from './v_0_2_1'
@@ -33,4 +33,5 @@ import { v_0_2_30 } from './v_0_2_30'
import { v_0_2_31 } from './v_0_2_31' import { v_0_2_31 } from './v_0_2_31'
import { v_0_2_32 } from './v_0_2_32' import { v_0_2_32 } from './v_0_2_32'
import { v_0_2_33 } from './v_0_2_33' import { v_0_2_33 } from './v_0_2_33'
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] import { v_0_2_34 } from './v_0_2_34'
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]
@@ -0,0 +1,13 @@
import { VersionInfo } from '@start9labs/start-sdk'
export const v_0_2_35 = VersionInfo.of({
version: '0.2.35:0',
releaseNotes: {
en_US:
"Exited positions now show the member's capital history up to the exit date: the collapsible History chart/table returns on the exited card, clipped at the exit — statements the eNAV keeps producing after the exit are recorded but never plotted, so the line ends at the exit instead of crashing to zero. History toggle now counts 'statements' (SPVs have event-driven statements, funds quarterly).",
},
migrations: {
up: async ({ effects }) => {},
down: async ({ effects }) => {},
},
})
+1 -1
View File
@@ -3,7 +3,7 @@
// - content-hashed /assets/* are cache-first (immutable, safe forever) // - content-hashed /assets/* are cache-first (immutable, safe forever)
// - /api/* is never cached // - /api/* is never cached
// Bump CACHE on each release so old entries are purged. // Bump CACHE on each release so old entries are purged.
const CACHE = 'ten31-portal-0.2.34' const CACHE = 'ten31-portal-0.2.35'
self.addEventListener('install', () => self.skipWaiting()) self.addEventListener('install', () => self.skipWaiting())
+26 -13
View File
@@ -256,18 +256,13 @@ function CapitalBlock({
() => [...accounts].sort((a, b) => a.as_of_date.localeCompare(b.as_of_date)), () => [...accounts].sort((a, b) => a.as_of_date.localeCompare(b.as_of_date)),
[accounts], [accounts],
); );
const chartPoints: CapitalPoint[] = history.map((a) => ({
date: a.as_of_date,
value: a.ending_balance_cents,
paidIn: a.contributions_cents,
distributions: a.distributions_cents,
}));
// Collapsed by default so the portal opens clean; the investor expands the trend per fund.
const [showChart, setShowChart] = useState(false);
// A sold/transferred stake: the fund's books show $0 with no distribution, which is not a // A sold/transferred stake: the fund's books show $0 with no distribution, which is not a
// loss. Show a quiet Exited badge and keep the documents; no balance, no gain/loss. // loss. Show a quiet Exited badge and their capital journey UP TO the exit — statements
// the eNAV keeps producing after that date are recorded but never plotted, so the line
// ends at the exit instead of crashing to zero.
if (latest.exited_on) { if (latest.exited_on) {
const clipped = history.filter((a) => a.as_of_date <= latest.exited_on!);
return ( return (
<div className={divider ? "mt-6 pt-5 border-t border-gray-100" : "mt-3"}> <div className={divider ? "mt-6 pt-5 border-t border-gray-100" : "mt-3"}>
{label && <p className="text-sm font-medium text-gray-700">{label}</p>} {label && <p className="text-sm font-medium text-gray-700">{label}</p>}
@@ -279,6 +274,7 @@ function CapitalBlock({
<p className="text-sm text-gray-400 mt-2"> <p className="text-sm text-gray-400 mt-2">
This position was sold or transferred. Documents remain available below. This position was sold or transferred. Documents remain available below.
</p> </p>
<HistorySection history={clipped} />
</div> </div>
); );
} }
@@ -314,7 +310,26 @@ function CapitalBlock({
</p> </p>
)} )}
{history.length > 1 && ( <HistorySection history={history} />
</div>
);
}
// Collapsible capital-over-time chart + statement table. Collapsed by default so the portal
// opens clean; the investor expands the trend per fund. SPVs get sparse event-driven points,
// funds quarterly ones — the chart plots whatever statements exist.
function HistorySection({ history }: { history: CapitalAccount[] }) {
const [showChart, setShowChart] = useState(false);
if (history.length < 2) return null;
const chartPoints: CapitalPoint[] = history.map((a) => ({
date: a.as_of_date,
value: a.ending_balance_cents,
paidIn: a.contributions_cents,
distributions: a.distributions_cents,
}));
return (
<div className="mt-5"> <div className="mt-5">
<button <button
type="button" type="button"
@@ -325,7 +340,7 @@ function CapitalBlock({
<span className={`transition-transform ${showChart ? "rotate-90" : ""}`}></span> <span className={`transition-transform ${showChart ? "rotate-90" : ""}`}></span>
History History
<span className="normal-case font-normal text-gray-400"> <span className="normal-case font-normal text-gray-400">
· {history.length} quarters · {history.length} statements
</span> </span>
</button> </button>
{showChart && ( {showChart && (
@@ -354,8 +369,6 @@ function CapitalBlock({
</div> </div>
)} )}
</div> </div>
)}
</div>
); );
} }
+1 -1
View File
@@ -1,4 +1,4 @@
// Bumped each release so the running build is visible in the UI. // 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, // If the number shown in the app doesn't match the installed s9pk version,
// the new frontend isn't actually being served. // the new frontend isn't actually being served.
export const APP_VERSION = "0.2.34"; export const APP_VERSION = "0.2.35";