Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3e18512
finalaudit: C1-K-56 released at 0306c985
claude Sep 27, 2026
46468cb
Page audit: a wide table no longer widens the page, and dead blog pho…
claude Sep 27, 2026
0ec8fa6
Page audit: the calendar no longer crashes, and a dialog open at firs…
claude Sep 27, 2026
87fcdd5
Page audit: a feature endpoint lets through whoever its page lets thr…
claude Sep 27, 2026
3a3ec37
Page audit: every page has one h1, and a cancelled sidebar read is re…
claude Sep 27, 2026
9c34870
Merge origin/main (19a24334) into the page-audit branch; record the a…
claude Sep 27, 2026
0c54f70
Page audit P-08: a first visit's second event is recorded, not answer…
claude Sep 27, 2026
6004cb1
Merge origin/main (06dd3f7e) into the page-audit branch
claude Sep 27, 2026
9271874
Page audit B7: every family route as a teen and as a child, recorded …
claude Sep 27, 2026
d9928a9
Page audit: an --interact mode that opens every tab, menu and dialog …
claude Sep 27, 2026
f4df5dd
Merge origin/main (7563462e, #591) into the page-audit branch; keep m…
claude Sep 27, 2026
b31f080
Merge origin/main (e14b12e8, #592) into the page-audit branch; restor…
claude Sep 27, 2026
fcbbb04
Merge remote-tracking branch 'origin/main' into claude/youthful-turin…
claude Sep 27, 2026
a84d786
Page audit P-09, P-10: /favicon.ico is served; an AI feature request …
claude Sep 27, 2026
4525583
Merge remote-tracking branch 'origin/main' into claude/youthful-turin…
claude Sep 27, 2026
2111264
Withdraw P-04's Modal-wide hydration change; keep main's call-site fi…
claude Sep 27, 2026
711b15c
P-10 review: an AI request row records a fixed label, never what the …
claude Sep 27, 2026
e52ec95
Merge origin/main (e70be830, #599) into the page-audit branch
claude Sep 27, 2026
c7789c4
Page audit B6a: every tab, dialog and button on the 278 family routes…
claude Sep 27, 2026
f7a3887
Merge origin/main (a6dcd18e, #600) into the page-audit branch
claude Sep 27, 2026
3faaba4
Page audit: claim B8's signed-in half (278 family routes in ten locales)
claude Sep 27, 2026
ceb97af
P-05 review: the super-admin bypass asks about the caller the endpoin…
claude Sep 27, 2026
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: 2 additions & 2 deletions app/(app)/admin/marketing/aeo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default async function AeoPage() {
<div className="space-y-5">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('adminMarketingAeo.marketingAeo')}</h1>
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('adminMarketingAeo.marketingAeo')}</h2>
<p className="mt-1 text-sm text-muted">{t('adminMarketingAeo.answerEngineOptimizationTrackTheQuestions')}</p>
</div>
<Link href="/admin/marketing/seo" className="text-sm font-medium text-brand-text hover:underline">SEO →</Link>
Expand Down Expand Up @@ -130,7 +130,7 @@ async function AdminAeoReadError() {
return (
<div className="module-page">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('aeo.marketingAeo')}</h1>
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('aeo.marketingAeo')}</h2>
<p className="mt-1 text-sm text-muted">{t('aeo.trackCustomerQuestionsAndStructured')}</p>
</div>
<ErrorState message={t('aeo.couldNotLoadAeoQuestions')} />
Expand Down
4 changes: 2 additions & 2 deletions app/(app)/admin/marketing/campaigns/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default async function CampaignDetailPage({ params }: { params: Promise<{
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<h1 className="text-xl font-bold">{c.name}</h1>
<h2 className="text-xl font-bold">{c.name}</h2>
<Badge tone="brand" className="capitalize">{c.status}</Badge>
</div>
<p className="mt-1 text-sm text-muted capitalize">{c.channel} · {c.type.replace('_', ' ')}</p>
Expand Down Expand Up @@ -110,7 +110,7 @@ async function CampaignDetailReadError() {
return (
<div className="module-page">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('campaigns.marketingCampaign')}</h1>
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('campaigns.marketingCampaign')}</h2>
<p className="mt-1 text-sm text-muted">{t('campaigns.reviewCampaignDetailsAudienceAnd')}</p>
</div>
<ErrorState message={t('campaigns.couldNotLoadThisMarketing')} />
Expand Down
4 changes: 2 additions & 2 deletions app/(app)/admin/marketing/lead-scores/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ async function ReadFailure() {
const t = await getTranslations();
return (
<div className="space-y-5 p-4 sm:p-6">
<h1 className="text-xl font-black sm:text-2xl">{t('leadScores.leadScores')}</h1>
<h2 className="text-xl font-black sm:text-2xl">{t('leadScores.leadScores')}</h2>
<ErrorState message={t('leadScores.couldNotLoadLeadScores')} />
<a href="/admin/marketing/lead-scores" className="text-sm font-medium text-brand-text underline">{t('leadScores.refreshLeadScores')}</a>
</div>
Expand Down Expand Up @@ -89,7 +89,7 @@ export default async function LeadScoresPage() {
<div className="space-y-5 p-4 sm:p-6">
<header className="flex flex-wrap items-start justify-between gap-3">
<div>
<h1 className="text-xl font-black sm:text-2xl">{t('adminMarketingLeadScores.leadScores')}</h1>
<h2 className="text-xl font-black sm:text-2xl">{t('adminMarketingLeadScores.leadScores')}</h2>
<p className="mt-1 max-w-2xl text-sm text-muted">{t('leadScores.everyContactScored0100')}</p>
</div>
<RecomputeButton />
Expand Down
4 changes: 2 additions & 2 deletions app/(app)/admin/marketing/seo/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default async function SeoPage() {
<div className="space-y-5">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('adminMarketingSeo.marketingSeo')}</h1>
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('adminMarketingSeo.marketingSeo')}</h2>
<p className="mt-1 text-sm text-muted">{t('adminMarketingSeo.trackSearchIntentKeywordsAndThe')}</p>
</div>
<Link href="/admin/marketing/aeo" className="text-sm font-medium text-brand-text hover:underline">AEO →</Link>
Expand Down Expand Up @@ -222,7 +222,7 @@ async function AdminSeoReadError() {
return (
<div className="module-page">
<div>
<h1 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('seo.marketingSeo')}</h1>
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{t('seo.marketingSeo')}</h2>
<p className="mt-1 text-sm text-muted">{t('seo.trackSearchIntentAndIndexable')}</p>
</div>
<ErrorState message={t('seo.couldNotLoadSeoKeywords')} />
Expand Down
4 changes: 2 additions & 2 deletions app/(app)/admin/marketing/visitor-intelligence/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default async function VisitorIntelligencePage() {
console.error('[visitor-intelligence] analytics read failed', failedMetric.error);
return (
<div className="space-y-5 p-4 sm:p-6">
<h1 className="text-xl font-black sm:text-2xl">{t('adminMarketingVisitorIntelligence.visitorIntelligence')}</h1>
<h2 className="text-xl font-black sm:text-2xl">{t('adminMarketingVisitorIntelligence.visitorIntelligence')}</h2>
<ErrorState message={t('visitorIntelligence.couldNotLoadVisitorIntelligence')} />
<a href="/admin/marketing/visitor-intelligence" className="text-sm font-medium text-brand-text underline">{t('adminMarketingVisitorIntelligence.refreshVisitorIntelligence')}</a>
</div>
Expand Down Expand Up @@ -96,7 +96,7 @@ export default async function VisitorIntelligencePage() {
return (
<div className="space-y-5 p-4 sm:p-6">
<header>
<h1 className="text-xl font-black sm:text-2xl">{t('adminMarketingVisitorIntelligence.visitorIntelligence')}</h1>
<h2 className="text-xl font-black sm:text-2xl">{t('adminMarketingVisitorIntelligence.visitorIntelligence')}</h2>
<p className="mt-1 max-w-2xl text-sm text-muted">
The privacy-first acquisition funnel end-to-end — anonymous visitors becoming identified,
profiled, scored, and engaged. All first-party and consent-gated; no fingerprinting.
Expand Down
11 changes: 10 additions & 1 deletion app/(app)/dashboard/assistant/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import type { Metadata } from 'next';
import { requireFeature } from '@/lib/supabase/auth';
import { AssistantModule } from '@/components/modules/assistant-module';
import { getTranslations } from '@/lib/i18n/server';

export const metadata: Metadata = { title: 'AI Assistant' };

// Free tier includes a metered AI assistant (10 requests/month). Access is open
// to any signed-in member; the monthly quota is enforced at the request layer.
export default async function AssistantPage() {
await requireFeature('/dashboard/assistant');
return <AssistantModule />;
const t = await getTranslations();
// The module's own heading is an h2 because it also renders inside the AI
// orb on other pages; the route's h1 lives here, for screen readers.
return (
<>
<h1 className="sr-only">{t('navShared.aiAssistant')}</h1>
<AssistantModule />
</>
);
}
13 changes: 11 additions & 2 deletions app/(app)/dashboard/grocery/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { Metadata } from 'next';
import { ShoppingModule } from '@/components/modules/shopping-module';
import { getTranslations } from '@/lib/i18n/server';

export const metadata: Metadata = { title: 'Shopping' };

export default function GroceryPage() {
return <ShoppingModule />;
export default async function GroceryPage() {
const t = await getTranslations();
// The lists module has no page title of its own (its headings are the list
// sidebar and the open list); the route's h1 lives here, for screen readers.
return (
<>
<h1 className="sr-only">{t('grocery.groceries')}</h1>
<ShoppingModule />
</>
);
}
2 changes: 1 addition & 1 deletion app/(app)/dashboard/paperwork/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export async function draftPaperworkReplyAction(itemId: string): Promise<DraftRe
// product, and not something to copy into a second table.
draft = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'paperwork.draft-reply', text: `Draft a reply to ${kindLabel(item.kind as PaperworkKind)}` },
{ feature: 'paperwork.draft-reply', text: 'Draft a reply' },
async (obs) => {
const provider = await resolveProvider();
const completion = await provider.complete({ system, messages: [{ role: 'user', content: user }], tools: [], maxTokens: 400 });
Expand Down
2 changes: 1 addition & 1 deletion app/(app)/marketplace/assistant-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export async function askMarketAssistantAction(
// like. The wrapper sits inside the swallow so the row separates them.
const text = await withAiRequest(
scopeFromUserContext(ctx, sb),
{ feature: 'marketplace.assistant', text: q.slice(0, 200) },
{ feature: 'marketplace.assistant', text: 'Marketplace assistant' },
async (obs) => {
const provider = await resolveProvider();
const messages: AIMessage[] = [
Expand Down
1 change: 1 addition & 0 deletions app/(app)/missions/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default async function NewMissionPage() {
return (
<div className="space-y-5">
<Link href="/missions" className="inline-flex items-center gap-1 text-sm text-muted hover:text-fg"><ArrowLeft className="h-4 w-4" /> {t('missionsNew.backToMissions')}</Link>
<h1 className="sr-only">{t('missions.newMission')}</h1>

<PlanGenerator members={kids.map((m) => ({ id: m.id, name: m.display_name }))} />

Expand Down
4 changes: 3 additions & 1 deletion app/(marketing)/f/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export default async function PublicFormPage({ params }: { params: Promise<{ id:
<>
<Section className="pt-20">
<SectionHeading
as="h1"
eyebrow={t('f.formUnavailable')}
title={title}
description={t('f.thisFormIsTemporarilyUnavailable')}
Expand All @@ -71,7 +72,8 @@ export default async function PublicFormPage({ params }: { params: Promise<{ id:
return (
<>
<Section className="pt-20">
<SectionHeading eyebrow={t('f.form')} title={title} description={description} />
{/* The form's own title is the page heading: this route has no other h1. */}
<SectionHeading as="h1" eyebrow={t('f.form')} title={title} description={description} />
<div className="mx-auto mt-12 max-w-xl">
<PublicForm
formId={form.id}
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/briefing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ ${UNTRUSTED_CONTENT_RULE}
// behaviour is unchanged.
briefing = await withAiRequest(
scope,
{ feature: `briefing.${type}`, text: `Generate ${type} briefing` },
{ feature: `briefing.${type}`, text: 'Generate a briefing' },
async (obs) => {
const provider = await resolveProvider();
const completion = await provider.complete({
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export async function POST(req: NextRequest) {
// errors still leaves the evidence.
await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'chat.assistant', text: message, kind: 'feature', conversationId },
{ feature: 'chat.assistant', text: 'Assistant chat', kind: 'feature', conversationId },
async (obs) => {
let content = '';
const actions: { name: string; args: Record<string, unknown>; result: unknown }[] = [];
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/chef/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export async function POST(req: Request) {
// so the row can tell the three apart.
const parsed = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'meals.chef', text: request.slice(0, 200) },
{ feature: 'meals.chef', text: 'Recipe from the AI chef' },
async (obs) => {
const completion = await (await resolveProvider()).complete({
system: buildChefSystem(),
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/home/diagnose/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export async function POST(req: Request) {
try {
text = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'home.diagnose', text: `Diagnose ${assetName || category || 'home item'}` },
{ feature: 'home.diagnose', text: 'Diagnose a home item' },
async (obs) => {
const completion = await (await resolveProvider()).complete({ system, messages: [{ role: 'user', content: userMsg }], tools: [] });
obs.used(completion.model ?? 'unknown', completion.usage);
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/home/find-pro/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function POST(req: Request) {
try {
text = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'home.find-pro', text: `Hire ${tradeLabel}` },
{ feature: 'home.find-pro', text: 'Find a pro' },
async (obs) => {
const completion = await (await resolveProvider()).complete({ system, messages: [{ role: 'user', content: userMsg }], tools: [] });
obs.used(completion.model ?? 'unknown', completion.usage);
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/home/forecast/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function POST(req: Request) {
try {
text = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'home.forecast', text: `Maintenance outlook for ${assets.length} assets` },
{ feature: 'home.forecast', text: 'Maintenance outlook' },
async (obs) => {
const completion = await (await resolveProvider()).complete({ system, messages: [{ role: 'user', content: `Home assets:\n${lines}` }], tools: [] });
obs.used(completion.model ?? 'unknown', completion.usage);
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/home/utility-savings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export async function POST() {
// reading a support ticket can tell it from "AI wasn't configured".
recommendations = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'home.utility-savings', text: `Utility savings across ${bills.length} bills` },
{ feature: 'home.utility-savings', text: 'Utility savings' },
async (obs) => {
const completion = await (await resolveProvider()).complete({ system, messages: [{ role: 'user', content: userMsg }], tools: [] });
obs.used(completion.model ?? 'unknown', completion.usage);
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/insights/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export async function POST(req: Request) {
try {
const text = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: `insights.${kind}`, text: `Insights: ${kind}` },
{ feature: `insights.${kind}`, text: 'Insights' },
async (obs) => {
const provider = await resolveProvider();
const completion = await provider.complete({
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/meals/nutrition/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export async function POST(req: Request) {
try {
parsed = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'meals.nutrition', text: `Nutrition for a ${subjectType}` },
{ feature: 'meals.nutrition', text: 'Nutrition breakdown' },
async (obs) => {
const completion = await (await resolveProvider()).complete({
system: SYSTEM, messages: [{ role: 'user', content: userMsg }], tools: [], maxTokens: 600,
Expand Down
2 changes: 1 addition & 1 deletion app/api/ai/trip/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function POST(req: NextRequest) {
// look like "no API key configured" too.
const parsed = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'travel.research', text: `Research a trip to ${input.destination}` },
{ feature: 'travel.research', text: 'Research a trip' },
async (obs) => {
const provider = await resolveProvider();
const completion = await provider.complete({
Expand Down
21 changes: 19 additions & 2 deletions app/api/mkt/track/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,28 @@ export async function POST(req: NextRequest) {
country: clean(body.country),
session_count: 1,
}).select('id').single();
if (createError) {
if (createError && createError.code === '23505') {
// Two events from a visitor's first page (the page view and the one
// fired beside it on /ai, say) both read "no visitor" and both insert;
// the loser hit the unique key and answered 503. The row it lost to is
// the visitor it was about to create: read it and record against it.
// Found by the 2026-09-27 page audit (P-08).
const { data: winner, error: winnerError } = await supabase
.from('mkt_visitors')
.select('id')
.eq('anonymous_id', anonymousId)
.maybeSingle();
if (winnerError || !winner) {
console.error('[mkt-track] visitor re-read after a concurrent create failed', winnerError ?? 'no row');
return NextResponse.json({ error: t('track.analyticsIsTemporarilyUnavailable') }, { status: 503 });
}
visitorId = winner.id;
} else if (createError) {
console.error('[mkt-track] visitor create failed', createError);
return NextResponse.json({ error: t('track.analyticsIsTemporarilyUnavailable') }, { status: 503 });
} else {
visitorId = created?.id ?? null;
}
visitorId = created?.id ?? null;
}
if (!visitorId) return NextResponse.json({ error: t('track.couldNotRecordVisitor') }, { status: 500 });

Expand Down
2 changes: 1 addition & 1 deletion app/api/vacations/ai/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ Costs/planned are whole US dollars. Keep itinerary day numbers between 1 and ${r
try {
const built = await withAiRequest(
scopeFromUserContext(ctx, supabase),
{ feature: 'vacations.build', text: `Build a plan for ${nights} nights` },
{ feature: 'vacations.build', text: 'Build a vacation plan' },
async (obs) => {
const provider = await resolveProvider();
const completion = await provider.complete({ system, messages: [{ role: 'user', content: user }], tools: [], maxTokens: 2000 });
Expand Down
19 changes: 16 additions & 3 deletions components/app/free-tier-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
// (user_preferences.notification_prefs.sidebarNav) so the rail follows the
// member across devices, with localStorage as an offline cache.

import { useCallback, useEffect, useId, useMemo, useState } from 'react';
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import { Star, ListPlus, ListX, Lock, RotateCcw } from 'lucide-react';
import { createClient } from '@/lib/supabase/client';
Expand Down Expand Up @@ -114,6 +114,8 @@ function useSidebarNav() {
const [loaded, setLoaded] = useState(false);
const [loadError, setLoadError] = useState(false);
const [reload, setReload] = useState(0);
const networkRetries = useRef(0);
const retryTimer = useRef<ReturnType<typeof setTimeout> | null>(null);

// Supabase is authoritative — reconcile once on mount (and on an explicit retry).
useEffect(() => {
Expand All @@ -124,6 +126,7 @@ function useSidebarNav() {
// (a failed re-read un-confirms whatever an earlier read established,
// as in Settings) and surface the failure.
if (error) { setLoadError(true); setLoaded(false); return; }
networkRetries.current = 0;
setLoadError(false);
if (nav != null) {
const clean = resolveNavKeys(nav, DEFAULT_SIDEBAR_NAV_KEYS, ALL_SERVICES_KEYS);
Expand All @@ -139,13 +142,23 @@ function useSidebarNav() {
// authoritative, and writable.
setLoaded(true);
}).catch((err) => {
// A server action the router cancelled (a client redirect landing while
// the read was in flight, e.g. /dashboard/vacations/<id> → /overview)
// rejects with a network TypeError while this sidebar stays mounted.
// That is not a read that failed: try again shortly, twice, before
// saying so. Anything else, or a third network failure, is reported.
if (active && err instanceof TypeError && networkRetries.current < 2) {
networkRetries.current += 1;
retryTimer.current = setTimeout(() => setReload((value) => value + 1), 800 * networkRetries.current);
return;
}
console.error('[sidebar] preference read failed', err);
if (active) { setLoadError(true); setLoaded(false); }
});
return () => { active = false; };
return () => { active = false; if (retryTimer.current) clearTimeout(retryTimer.current); };
}, [reload]);

const retryLoad = useCallback(() => { setLoadError(false); setReload((value) => value + 1); }, []);
const retryLoad = useCallback(() => { networkRetries.current = 0; setLoadError(false); setReload((value) => value + 1); }, []);

// Live update when a layout change is broadcast in this tab (Settings editor
// saving, or the other FreeTierSidebar instance pinning).
Expand Down
Loading
Loading