Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
type TimeFormat,
type RefreshIntervalSeconds,
REFRESH_INTERVAL_OPTIONS,
notifySettingsUpdated,
} from "@/hooks/useSettings";


Expand Down Expand Up @@ -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.
}
Expand Down
106 changes: 104 additions & 2 deletions app/stream/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -451,14 +452,29 @@ export default function StreamPage() {
<td className="py-2.5 text-gray-400 dark:text-gray-500 w-40">
Rate
</td>
<td className="py-2.5 font-mono text-black dark:text-white text-right">{`${fromStroops(info.ratePerSecond)} / sec`}</td>
<td className="py-2.5 font-mono text-black dark:text-white text-right">
{`${fromStroops(info.ratePerSecond)} / sec`}
{advancedMode && (
<span
className="block text-[11px] text-gray-400 dark:text-gray-500"
title={`${info.ratePerSecond.toString()} stroops per second`}
>
{info.ratePerSecond.toString()} stroops/s
</span>
)}
</td>
</tr>
<tr>
<td className="py-2.5 text-gray-400 dark:text-gray-500 w-40">
Total deposited
</td>
<td className="py-2.5 font-mono text-black dark:text-white text-right">
{fromStroops(totalDeposited)}
{advancedMode && (
<span className="block text-[11px] text-gray-400 dark:text-gray-500">
{totalDeposited.toString()} stroops
</span>
)}
</td>
</tr>
<tr>
Expand All @@ -467,6 +483,11 @@ export default function StreamPage() {
</td>
<td className="py-2.5 font-mono text-black dark:text-white text-right">
{fromStroops(info.withdrawn)}
{advancedMode && (
<span className="block text-[11px] text-gray-400 dark:text-gray-500">
{info.withdrawn.toString()} stroops
</span>
)}
</td>
</tr>
<tr>
Expand Down Expand Up @@ -499,6 +520,87 @@ export default function StreamPage() {
</table>
</Card>

{/* 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 && (
<Card className="mb-6">
<h2 className="text-sm font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-3">
Advanced details
</h2>
<dl className="space-y-2.5 text-sm">
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Stream contract
</dt>
<dd className="text-right break-all font-mono text-xs text-black dark:text-white">
<CopyableAddress address={streamAddress} />
</dd>
</div>
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Token contract
</dt>
<dd className="text-right break-all font-mono text-xs text-black dark:text-white">
<CopyableAddress address={info.token} />
</dd>
</div>
{tryGetFactoryContractId() && (
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Factory contract
</dt>
<dd className="text-right break-all font-mono text-xs text-black dark:text-white">
<CopyableAddress
address={tryGetFactoryContractId() as string}
/>
</dd>
</div>
)}
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Rate (stroops/s)
</dt>
<dd className="text-right font-mono text-xs text-black dark:text-white break-all">
{info.ratePerSecond.toString()}
</dd>
</div>
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Withdrawable (stroops)
</dt>
<dd className="text-right font-mono text-xs text-black dark:text-white break-all">
{withdrawable.toString()}
</dd>
</div>
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Withdrawn (stroops)
</dt>
<dd className="text-right font-mono text-xs text-black dark:text-white break-all">
{info.withdrawn.toString()}
</dd>
</div>
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
Start (unix)
</dt>
<dd className="text-right font-mono text-xs text-black dark:text-white">
{info.startTime}
</dd>
</div>
<div className="flex items-start justify-between gap-3">
<dt className="text-gray-400 dark:text-gray-500 shrink-0">
End (unix)
</dt>
<dd className="text-right font-mono text-xs text-black dark:text-white">
{info.endTime}
</dd>
</div>
</dl>
</Card>
)}

{/* Actions */}
{(isSender || isRecipient) && (
<div className="print:hidden">
Expand Down
35 changes: 35 additions & 0 deletions app/streams/compare/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Column[]>([]);
const [loading, setLoading] = useState(false);
Expand Down Expand Up @@ -223,6 +225,39 @@ function CompareView() {
))}
</tr>
))}
{/* Raw stroop rates + full token contract addresses — advanced mode only (#586) */}
{advancedMode && (
<>
<tr className="border-b last:border-b-0 border-gray-100 dark:border-gray-800/60">
<th scope="row" className="text-left p-3 text-xs font-medium text-gray-400 dark:text-gray-500 whitespace-nowrap align-top">
Rate (stroops/s)
</th>
{columns.map((col) => (
<td key={col.id} className="p-3 align-top font-mono text-xs break-all">
{col.kind === 'error' ? (
<span className="text-gray-300 dark:text-gray-700">—</span>
) : (
col.info.ratePerSecond.toString()
)}
</td>
))}
</tr>
<tr className="border-b last:border-b-0 border-gray-100 dark:border-gray-800/60">
<th scope="row" className="text-left p-3 text-xs font-medium text-gray-400 dark:text-gray-500 whitespace-nowrap align-top">
Token contract
</th>
{columns.map((col) => (
<td key={col.id} className="p-3 align-top font-mono text-xs break-all">
{col.kind === 'error' ? (
<span className="text-gray-300 dark:text-gray-700">—</span>
) : (
col.info.token
)}
</td>
))}
</tr>
</>
)}
</tbody>
</table>
</div>
Expand Down
12 changes: 11 additions & 1 deletion components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand All @@ -24,6 +25,15 @@ export function Navbar() {
const mobileMenuRef = useRef<HTMLElement>(null);
const previousFocusRef = useRef<HTMLElement | null>(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(() => {
Expand Down
23 changes: 18 additions & 5 deletions components/stream/StreamCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +39,7 @@ export function StreamCard({
streamAddress,
}: StreamCardProps) {
const rateFormatted = fromStroops(ratePerSecond);
const { advancedMode } = useSettings();
const [note, setNote] = useState<string | null>(null);

// Load note from storage
Expand Down Expand Up @@ -85,11 +87,22 @@ export function StreamCard({
</div>

{/* Rate number centered, green text */}
<div
className="amount text-xs sm:text-sm font-bold text-green-600 dark:text-green-400 truncate text-center px-1"
aria-label={`streaming rate: ${rateFormatted} per second`}
>
{rateFormatted}/s
<div className="flex flex-col items-center px-1 min-w-0">
<div
className="amount text-xs sm:text-sm font-bold text-green-600 dark:text-green-400 truncate text-center"
aria-label={`streaming rate: ${rateFormatted} per second`}
>
{rateFormatted}/s
</div>
{/* Raw stroop rate — only in advanced mode (#586) */}
{advancedMode && (
<span
className="font-mono text-[10px] text-gray-400 dark:text-gray-500 truncate"
title={`${ratePerSecond.toString()} stroops per second`}
>
{ratePerSecond.toString()} stroops/s
</span>
)}
</div>

<div className="shrink-0">
Expand Down
37 changes: 33 additions & 4 deletions hooks/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,21 @@ 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";

const DEFAULTS: PublicSettings = {
timeFormat: "absolute",
autoRefreshInterval: 0,
advancedMode: false,
};

function readFromStorage(): PublicSettings {
Expand All @@ -55,25 +63,46 @@ function readFromStorage(): PublicSettings {
)
? (parsed.autoRefreshInterval as RefreshIntervalSeconds)
: 0,
advancedMode: parsed.advancedMode === true,
};
} catch {
return DEFAULTS;
}
}

/**
* 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<PublicSettings>(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;
Expand Down