diff --git a/app/settings/page.tsx b/app/settings/page.tsx
index 96f6767..97a694c 100644
--- a/app/settings/page.tsx
+++ b/app/settings/page.tsx
@@ -18,6 +18,7 @@ import {
type TimeFormat,
type RefreshIntervalSeconds,
REFRESH_INTERVAL_OPTIONS,
+ notifySettingsUpdated,
} from "@/hooks/useSettings";
@@ -102,6 +103,9 @@ export default function SettingsPage() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
+ // Wake up mounted useSettings() subscribers in this tab — the
+ // `storage` event only fires in *other* tabs (#586).
+ notifySettingsUpdated();
} catch {
// Storage can be unavailable in private browsing or embedded webviews.
}
diff --git a/app/stream/[id]/page.tsx b/app/stream/[id]/page.tsx
index 8740708..29f338f 100644
--- a/app/stream/[id]/page.tsx
+++ b/app/stream/[id]/page.tsx
@@ -27,6 +27,7 @@ import {
import { useNetworkStatus } from "@/hooks/useNetworkStatus";
import { fromStroops, formatTimestamp, truncateAddress } from "@/lib/format";
import { tokenByAddress } from "@/lib/tokens";
+import { tryGetFactoryContractId } from "@/lib/env";
import { useSettings, MIN_REFRESH_INTERVAL_S } from "@/hooks/useSettings";
// ── Types ────────────────────────────────────────────────────────────────────
@@ -56,7 +57,7 @@ export default function StreamPage() {
// NetworkTroubleBanner — defer to it rather than printing a raw
// "circuit breaker open" string here.
const { status: networkStatus } = useNetworkStatus();
- const { autoRefreshInterval } = useSettings();
+ const { autoRefreshInterval, advancedMode } = useSettings();
const mounted = useRef(true);
const loadSeq = useRef(0);
@@ -451,7 +452,17 @@ export default function StreamPage() {
Rate
|
- {`${fromStroops(info.ratePerSecond)} / sec`} |
+
+ {`${fromStroops(info.ratePerSecond)} / sec`}
+ {advancedMode && (
+
+ {info.ratePerSecond.toString()} stroops/s
+
+ )}
+ |
|
@@ -459,6 +470,11 @@ export default function StreamPage() {
|
{fromStroops(totalDeposited)}
+ {advancedMode && (
+
+ {totalDeposited.toString()} stroops
+
+ )}
|
@@ -467,6 +483,11 @@ export default function StreamPage() {
|
{fromStroops(info.withdrawn)}
+ {advancedMode && (
+
+ {info.withdrawn.toString()} stroops
+
+ )}
|
@@ -499,6 +520,87 @@ export default function StreamPage() {
+ {/* Advanced details — only when advanced mode is enabled (#586).
+ Surfaces raw stroop values and full contract addresses inline so
+ power users can verify on-chain state without leaving the page. */}
+ {advancedMode && (
+
+
+ Advanced details
+
+
+
+
-
+ Stream contract
+
+ -
+
+
+
+
+
-
+ Token contract
+
+ -
+
+
+
+ {tryGetFactoryContractId() && (
+
+
-
+ Factory contract
+
+ -
+
+
+
+ )}
+
+
-
+ Rate (stroops/s)
+
+ -
+ {info.ratePerSecond.toString()}
+
+
+
+
-
+ Withdrawable (stroops)
+
+ -
+ {withdrawable.toString()}
+
+
+
+
-
+ Withdrawn (stroops)
+
+ -
+ {info.withdrawn.toString()}
+
+
+
+
-
+ Start (unix)
+
+ -
+ {info.startTime}
+
+
+
+
-
+ End (unix)
+
+ -
+ {info.endTime}
+
+
+
+
+ )}
+
{/* Actions */}
{(isSender || isRecipient) && (
diff --git a/app/streams/compare/page.tsx b/app/streams/compare/page.tsx
index f44af68..8487779 100644
--- a/app/streams/compare/page.tsx
+++ b/app/streams/compare/page.tsx
@@ -8,6 +8,7 @@ import { ArrowLeft } from 'lucide-react';
import { Badge } from '@/components/ui/Badge';
import { StreamProgressBar } from '@/components/stream/StreamProgressBar';
import { useWallet } from '@/contexts/WalletContext';
+import { useSettings } from '@/hooks/useSettings';
import { getStreamAddress, getStreamInfo, type StreamInfo } from '@/lib/stream';
import { fromStroops, formatDuration, formatTimestamp, truncateAddress } from '@/lib/format';
import { tokenByAddress } from '@/lib/tokens';
@@ -124,6 +125,7 @@ function CompareView() {
const searchParams = useSearchParams();
const idsParam = searchParams.get('ids');
const { publicKey, connected } = useWallet();
+ const { advancedMode } = useSettings();
const [columns, setColumns] = useState([]);
const [loading, setLoading] = useState(false);
@@ -223,6 +225,39 @@ function CompareView() {
))}
))}
+ {/* Raw stroop rates + full token contract addresses — advanced mode only (#586) */}
+ {advancedMode && (
+ <>
+
+ |
+ Rate (stroops/s)
+ |
+ {columns.map((col) => (
+
+ {col.kind === 'error' ? (
+ —
+ ) : (
+ col.info.ratePerSecond.toString()
+ )}
+ |
+ ))}
+
+
+ |
+ Token contract
+ |
+ {columns.map((col) => (
+
+ {col.kind === 'error' ? (
+ —
+ ) : (
+ col.info.token
+ )}
+ |
+ ))}
+
+ >
+ )}
diff --git a/components/Navbar.tsx b/components/Navbar.tsx
index 94b92e3..e9a581b 100644
--- a/components/Navbar.tsx
+++ b/components/Navbar.tsx
@@ -9,8 +9,9 @@ import { ThemeToggle } from '@/components/ThemeToggle';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { NetworkBadge } from '@/components/NetworkBadge';
import { useSelectedNetwork } from '@/hooks/useSelectedNetwork';
+import { useSettings } from '@/hooks/useSettings';
-const NAV = [
+const BASE_NAV = [
{ href: '/streams', label: 'Streams' },
{ href: '/transactions', label: 'History' },
{ href: '/create', label: 'Create' },
@@ -24,6 +25,15 @@ export function Navbar() {
const mobileMenuRef = useRef(null);
const previousFocusRef = useRef(null);
const network = useSelectedNetwork();
+ const { advancedMode } = useSettings();
+
+ // The side-by-side compare view is an advanced/power-user tool, so it is
+ // only linked in the nav when advanced mode is enabled (#586). The route
+ // itself stays directly reachable via /streams/compare?ids=… so shared
+ // links keep working.
+ const NAV = advancedMode
+ ? [...BASE_NAV, { href: '/streams/compare', label: 'Compare' }]
+ : BASE_NAV;
// Close the mobile menu whenever the route changes (e.g. after a nav tap).
useEffect(() => {
diff --git a/components/stream/StreamCard.tsx b/components/stream/StreamCard.tsx
index 3eb9f32..34f2395 100644
--- a/components/stream/StreamCard.tsx
+++ b/components/stream/StreamCard.tsx
@@ -7,6 +7,7 @@ import { StreamProgressBar } from "@/components/stream/StreamProgressBar";
import { fromStroops } from "@/lib/format";
import { CopyAddress } from "@/components/ui/CopyAddress";
import { getStreamNote } from "@/lib/stream-notes-storage";
+import { useSettings } from "@/hooks/useSettings";
interface StreamCardProps {
id: string;
@@ -38,6 +39,7 @@ export function StreamCard({
streamAddress,
}: StreamCardProps) {
const rateFormatted = fromStroops(ratePerSecond);
+ const { advancedMode } = useSettings();
const [note, setNote] = useState(null);
// Load note from storage
@@ -85,11 +87,22 @@ export function StreamCard({
{/* Rate number centered, green text */}
-
- {rateFormatted}/s
+
+
+ {rateFormatted}/s
+
+ {/* Raw stroop rate — only in advanced mode (#586) */}
+ {advancedMode && (
+
+ {ratePerSecond.toString()} stroops/s
+
+ )}
diff --git a/hooks/useSettings.ts b/hooks/useSettings.ts
index 725aa87..f23a5e0 100644
--- a/hooks/useSettings.ts
+++ b/hooks/useSettings.ts
@@ -33,6 +33,13 @@ export interface PublicSettings {
timeFormat: TimeFormat;
/** 0 = disabled */
autoRefreshInterval: RefreshIntervalSeconds;
+ /**
+ * When true, advanced/debugging details are surfaced across the app:
+ * raw stroop amounts, full contract addresses, and the stream compare
+ * view link. Mirrors the `advancedMode` toggle on the settings page
+ * (issue #586).
+ */
+ advancedMode: boolean;
}
const STORAGE_KEY = "conduit:settings";
@@ -40,6 +47,7 @@ const STORAGE_KEY = "conduit:settings";
const DEFAULTS: PublicSettings = {
timeFormat: "absolute",
autoRefreshInterval: 0,
+ advancedMode: false,
};
function readFromStorage(): PublicSettings {
@@ -55,6 +63,7 @@ function readFromStorage(): PublicSettings {
)
? (parsed.autoRefreshInterval as RefreshIntervalSeconds)
: 0,
+ advancedMode: parsed.advancedMode === true,
};
} catch {
return DEFAULTS;
@@ -62,18 +71,38 @@ function readFromStorage(): PublicSettings {
}
/**
- * Returns current public settings, and re-reads them whenever another tab or
- * the settings page writes to localStorage (via the `storage` event).
+ * Custom event fired on `window` after a same-tab settings write so mounted
+ * components re-read without waiting for a cross-tab `storage` event (which
+ * by spec never fires in the tab that performed the write). See #586.
+ */
+export const SETTINGS_UPDATED_EVENT = "conduit:settings-updated";
+
+/** Notify mounted `useSettings` subscribers to re-read from localStorage. */
+export function notifySettingsUpdated(): void {
+ if (typeof window !== "undefined") {
+ window.dispatchEvent(new Event(SETTINGS_UPDATED_EVENT));
+ }
+}
+
+/**
+ * Returns current public settings, and re-reads them whenever any tab (via
+ * the `storage` event) or the settings page in this tab (via
+ * {@link notifySettingsUpdated}) writes to localStorage.
*/
export function useSettings(): PublicSettings {
const [settings, setSettings] = useState
(readFromStorage);
useEffect(() => {
+ const refresh = () => setSettings(readFromStorage());
const onStorage = (e: StorageEvent) => {
- if (e.key === STORAGE_KEY) setSettings(readFromStorage());
+ if (e.key === STORAGE_KEY) refresh();
};
window.addEventListener("storage", onStorage);
- return () => window.removeEventListener("storage", onStorage);
+ window.addEventListener(SETTINGS_UPDATED_EVENT, refresh);
+ return () => {
+ window.removeEventListener("storage", onStorage);
+ window.removeEventListener(SETTINGS_UPDATED_EVENT, refresh);
+ };
}, []);
return settings;