diff --git a/apps/cli/src/tui/statusFreshness.test.ts b/apps/cli/src/tui/statusFreshness.test.ts
index 4187d1d..d450307 100644
--- a/apps/cli/src/tui/statusFreshness.test.ts
+++ b/apps/cli/src/tui/statusFreshness.test.ts
@@ -207,22 +207,24 @@ describe('computeStatusFreshness — stale payload', () => {
it('badges stale once age exceeds the threshold', () => {
const now = 1_700_000_000_000;
+ const ageMs = TRADE_STALE_AFTER_MS + 42_000; // well past the threshold
const out = computeStatusFreshness({
...BASE,
dataLoading: false,
- lastTickMs: now - (TRADE_STALE_AFTER_MS + 42_000), // 52s past last tick
+ lastTickMs: now - ageMs,
nowMs: now,
});
+ const sec = Math.round(ageMs / 1000);
expect(out.isStale).toBe(true);
- expect(out.staleSec).toBe(52);
- expect(out.freshnessStr).toContain('stale 52s');
+ expect(out.staleSec).toBe(sec);
+ expect(out.freshnessStr).toContain(`stale ${sec}s`);
});
it('renders real (stale) PnL so the operator can still see last-known numbers', () => {
const now = 1_700_000_000_000;
const out = computeStatusFreshness({
dataLoading: false,
- lastTickMs: now - 30_000,
+ lastTickMs: now - (TRADE_STALE_AFTER_MS + 5_000), // clearly past the stale threshold
nowMs: now,
realizedPnl: 7.0,
realizedPnlPct: 1.1,
diff --git a/apps/web/src/app/api/auth/signup/route.ts b/apps/web/src/app/api/auth/signup/route.ts
index 3aafc39..88dc75a 100644
--- a/apps/web/src/app/api/auth/signup/route.ts
+++ b/apps/web/src/app/api/auth/signup/route.ts
@@ -27,6 +27,8 @@ export async function POST(req: NextRequest) {
);
}
+ if (!data.user) return Response.json({ error: 'no user returned' }, { status: 500 });
+
const payload = {
user: { id: data.user.id, email: data.user.email },
session: data.session && {
diff --git a/apps/web/src/app/console/page.tsx b/apps/web/src/app/console/page.tsx
index 55e4656..ac6c207 100644
--- a/apps/web/src/app/console/page.tsx
+++ b/apps/web/src/app/console/page.tsx
@@ -4,6 +4,7 @@ import Image from 'next/image';
import { createServerSupabase } from '@/lib/supabase';
import { ConsoleClient } from './console-client';
import { RenewalBanner } from '@/components/renewal-banner';
+import { SignOutForm } from '@/components/sign-out-form';
export const dynamic = 'force-dynamic';
@@ -26,9 +27,7 @@ export default async function ConsolePage() {
Store
Settings
{user.email}
-
+
diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx
index 5d88a0a..96bf289 100644
--- a/apps/web/src/app/dashboard/page.tsx
+++ b/apps/web/src/app/dashboard/page.tsx
@@ -6,6 +6,7 @@ import { TradingChart } from './trading-chart';
import { DashboardSummary } from './dashboard-summary';
import { GrowthProjection } from './growth-projection';
import { RenewalBanner } from '@/components/renewal-banner';
+import { SignOutForm } from '@/components/sign-out-form';
export const dynamic = 'force-dynamic';
@@ -29,9 +30,7 @@ export default async function DashboardPage() {
Console →
Settings
{user.email}
-
+
diff --git a/apps/web/src/app/dashboard/projections/page.tsx b/apps/web/src/app/dashboard/projections/page.tsx
index f7d07fb..eda51b5 100644
--- a/apps/web/src/app/dashboard/projections/page.tsx
+++ b/apps/web/src/app/dashboard/projections/page.tsx
@@ -3,6 +3,7 @@ import { redirect } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import { ProjectionsClient } from './projections-client';
+import { SignOutForm } from '@/components/sign-out-form';
export const dynamic = 'force-dynamic';
@@ -25,9 +26,7 @@ export default async function ProjectionsPage() {
Console →
Settings
{user.email}
-
+
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index cfb6d3f..3db21d9 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -6,6 +6,7 @@ import {
UniswapLogo, ZeroExLogo, OneInchLogo, JupiterLogo, PumpFunLogo,
UniswapV4Logo, PancakeSwapLogo, RaydiumLogo, OrcaLogo, AerodromeLogo,
} from './_components/brand-logos';
+import { SignOutForm } from '@/components/sign-out-form';
export const dynamic = 'force-dynamic';
@@ -127,9 +128,7 @@ export default async function LandingPage() {
Console
Settings
{user.email}
-
+
>
) : (
<>
diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx
index 716a483..ef4fc43 100644
--- a/apps/web/src/app/settings/page.tsx
+++ b/apps/web/src/app/settings/page.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link';
import Image from 'next/image';
import { createServerSupabase } from '@/lib/supabase';
import { SettingsClient } from './settings-client';
+import { SignOutForm } from '@/components/sign-out-form';
export const dynamic = 'force-dynamic';
@@ -24,9 +25,7 @@ export default async function SettingsPage() {
{user.email}
-
+
diff --git a/apps/web/src/app/store/page.tsx b/apps/web/src/app/store/page.tsx
index c7e5cd2..0d269f1 100644
--- a/apps/web/src/app/store/page.tsx
+++ b/apps/web/src/app/store/page.tsx
@@ -5,6 +5,7 @@ import { PLUGIN_CATALOG, type CatalogEntry } from '@b1dz/core';
import { createServerSupabase } from '@/lib/supabase';
import { coinpayConfigured } from '@/lib/coinpay-client';
import { InstallButton } from './install-button';
+import { SignOutForm } from '@/components/sign-out-form';
export const metadata: Metadata = {
title: 'b1dz Store — Plugin Marketplace',
@@ -78,9 +79,7 @@ export default async function StorePage() {
Dashboard
Settings
{user.email}
-
+
>
) : (
<>
diff --git a/apps/web/src/components/sign-out-form.tsx b/apps/web/src/components/sign-out-form.tsx
new file mode 100644
index 0000000..725ee82
--- /dev/null
+++ b/apps/web/src/components/sign-out-form.tsx
@@ -0,0 +1,23 @@
+'use client';
+
+/**
+ * Sign-out button that clears this browser's b1dz client-side cache before the
+ * server logout. Logout is a server form POST (clears the session cookie), which
+ * can't touch localStorage — so without this, cached dashboard state (e.g.
+ * b1dz:source-state:*) would linger for the next account on a shared browser.
+ * Defense-in-depth on top of the per-user cache keying.
+ */
+export function SignOutForm({ className }: { className?: string }) {
+ const clearClientCache = () => {
+ try {
+ for (const key of Object.keys(window.localStorage)) {
+ if (key.startsWith('b1dz:')) window.localStorage.removeItem(key);
+ }
+ } catch { /* private mode / quota */ }
+ };
+ return (
+
+ );
+}
diff --git a/packages/core/src/runtime-cache.test.ts b/packages/core/src/runtime-cache.test.ts
index db71231..c114812 100644
--- a/packages/core/src/runtime-cache.test.ts
+++ b/packages/core/src/runtime-cache.test.ts
@@ -30,9 +30,11 @@ describe('stripLiveSourceState', () => {
},
],
},
- // Heavy/noisy tick data should still be stripped from DB fallback.
+ // Heavy/noisy tick data (prices, rawLog) is stripped from the DB fallback.
prices: [{ exchange: 'kraken', pair: 'BTC-USD', bid: 101, ask: 102 }],
rawLog: [{ at: 'now', text: 'noise' }],
+ // tradeState is intentionally KEPT: closedTrades is persistent history the
+ // UI needs even when the runtime cache has expired.
tradeState: { closedTrades: [{ pair: 'BTC-USD' }] },
};
@@ -42,6 +44,6 @@ describe('stripLiveSourceState', () => {
expect(stripped.tradeStatus).toEqual(payload.tradeStatus);
expect(stripped.prices).toBeUndefined();
expect(stripped.rawLog).toBeUndefined();
- expect(stripped.tradeState).toBeUndefined();
+ expect(stripped.tradeState).toEqual(payload.tradeState);
});
});
diff --git a/packages/core/src/runtime-cache.ts b/packages/core/src/runtime-cache.ts
index 0d35113..dbbf9db 100644
--- a/packages/core/src/runtime-cache.ts
+++ b/packages/core/src/runtime-cache.ts
@@ -17,6 +17,7 @@ const LIVE_SOURCE_STATE_FIELDS = new Set([
'activityLog',
'openOrders',
'opportunities',
+ 'prices',
'rawLog',
'recentTrades',
'signals',
diff --git a/packages/source-dealdash/src/strategy/decide.test.ts b/packages/source-dealdash/src/strategy/decide.test.ts
index fe9841c..4464e18 100644
--- a/packages/source-dealdash/src/strategy/decide.test.ts
+++ b/packages/source-dealdash/src/strategy/decide.test.ts
@@ -54,9 +54,9 @@ function ctxBase(over: Partial): DecisionContext {
}
const book = (r: { decisions: Decision[] }, id: number) =>
- r.decisions.find(d => d.kind === 'book' && d.auctionId === id);
+ r.decisions.find((d): d is Extract => d.kind === 'book' && d.auctionId === id);
const cancel = (r: { decisions: Decision[] }, id: number) =>
- r.decisions.find(d => d.kind === 'cancel' && d.auctionId === id);
+ r.decisions.find((d): d is Extract => d.kind === 'cancel' && d.auctionId === id);
// ---------- balance state transitions ----------