Polish: per-route titles and a way to sign out

There was no sign-out control anywhere. Easy to miss when you develop
permanently signed in, and a stranded feeling for anyone on a shared machine.
Clearing the identity provider's session is sufficient — PIG holds no session
of its own — and the auth listener returns to sign-in without a reload. A hard
redirect follows regardless, so a failed provider call cannot leave a
half-signed-out interface.

Every route now sets its own document title. A single static title makes
browser history and a wall of tabs useless: every entry reads "pig" and nobody
can tell the margin view from the pipeline.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 19:49:32 -07:00
parent 46d7d80f1b
commit d154a78d48
7 changed files with 78 additions and 2 deletions
+18
View File
@@ -0,0 +1,18 @@
/**
* Per-route document titles.
*
* A single static title makes browser history and a wall of open tabs useless
* — every entry reads "pig" and nobody can tell the margin view from the
* pipeline. The page name leads, because that is the part a tab strip has room
* to show before truncating.
*/
import { useEffect } from 'react';
export function usePageTitle(title?: string): void {
useEffect(() => {
document.title = title ? `${title} · pig` : 'pig — Prime Intellect Growth';
return () => {
document.title = 'pig — Prime Intellect Growth';
};
}, [title]);
}
+2
View File
@@ -6,6 +6,7 @@ import { useQuery } from '@tanstack/react-query';
import { Building2 } from 'lucide-react'; import { Building2 } from 'lucide-react';
import { get, relativeTime } from '@/lib/api'; import { get, relativeTime } from '@/lib/api';
import { Badge, ConfidenceBadge, EmptyState, Input, Skeleton } from '@/components/ui'; import { Badge, ConfidenceBadge, EmptyState, Input, Skeleton } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
interface Account { interface Account {
id: string; id: string;
@@ -20,6 +21,7 @@ interface Account {
} }
export function Accounts() { export function Accounts() {
usePageTitle('Accounts');
const [side, setSide] = useState<'all' | 'supply' | 'demand'>('all'); const [side, setSide] = useState<'all' | 'supply' | 'demand'>('all');
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
+2
View File
@@ -9,6 +9,7 @@ import { useState } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery } from '@tanstack/react-query';
import { Search, Server, Zap } from 'lucide-react'; import { Search, Server, Zap } from 'lucide-react';
import { compactNumber, get, money, percent, post, shortDate } from '@/lib/api'; import { compactNumber, get, money, percent, post, shortDate } from '@/lib/api';
import { usePageTitle } from '@/lib/title';
import { import {
Badge, Badge,
Button, Button,
@@ -42,6 +43,7 @@ interface AvailabilityRow {
type MatchRow = AvailabilityRow & { score: number; rationale: string[] }; type MatchRow = AvailabilityRow & { score: number; rationale: string[] };
export function Capacity() { export function Capacity() {
usePageTitle('Capacity');
const [tab, setTab] = useState<'available' | 'match'>('available'); const [tab, setTab] = useState<'available' | 'match'>('available');
return ( return (
+2
View File
@@ -8,6 +8,7 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { compactNumber, get, money, moneyExact, percent } from '@/lib/api'; import { compactNumber, get, money, moneyExact, percent } from '@/lib/api';
import { Card, CardContent, CardHeader, CardTitle, EmptyState, Skeleton, Stat } from '@/components/ui'; import { Card, CardContent, CardHeader, CardTitle, EmptyState, Skeleton, Stat } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
interface MarginReport { interface MarginReport {
totals: { totals: {
@@ -36,6 +37,7 @@ interface MarginReport {
} }
export function Margin() { export function Margin() {
usePageTitle('Margin');
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: ['margin'], queryKey: ['margin'],
queryFn: () => get<MarginReport>('/api/capacity/margin'), queryFn: () => get<MarginReport>('/api/capacity/margin'),
+2
View File
@@ -10,6 +10,7 @@ import { AlertTriangle, ArrowRight, Server, TrendingUp } from 'lucide-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { compactNumber, get, money, percent, relativeTime } from '@/lib/api'; import { compactNumber, get, money, percent, relativeTime } from '@/lib/api';
import { Badge, Card, CardContent, CardHeader, CardTitle, EmptyState, Skeleton, Stat } from '@/components/ui'; import { Badge, Card, CardContent, CardHeader, CardTitle, EmptyState, Skeleton, Stat } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
interface Dashboard { interface Dashboard {
me: { name: string; teams: { team: string; role: string }[] }; me: { name: string; teams: { team: string; role: string }[] };
@@ -44,6 +45,7 @@ interface Dashboard {
} }
export function Overview() { export function Overview() {
usePageTitle('Overview');
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
queryKey: ['dashboard'], queryKey: ['dashboard'],
queryFn: () => get<Dashboard>('/api/dashboard'), queryFn: () => get<Dashboard>('/api/dashboard'),
+3
View File
@@ -10,6 +10,7 @@ import { useMemo, useState } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { get, money, relativeTime } from '@/lib/api'; import { get, money, relativeTime } from '@/lib/api';
import { Badge, Card, EmptyState, Skeleton } from '@/components/ui'; import { Badge, Card, EmptyState, Skeleton } from '@/components/ui';
import { usePageTitle } from '@/lib/title';
interface DemandDeal { interface DemandDeal {
id: string; id: string;
@@ -126,6 +127,8 @@ function PipelineBoard<T extends { id: string; stage: string; updatedAt: string
endpoint: string; endpoint: string;
renderCard: (deal: T, accountName: string | null) => React.ReactNode; renderCard: (deal: T, accountName: string | null) => React.ReactNode;
}) { }) {
usePageTitle(title);
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
queryKey: [endpoint], queryKey: [endpoint],
queryFn: () => get<Board<T>>(endpoint), queryFn: () => get<Board<T>>(endpoint),
+49 -2
View File
@@ -6,12 +6,13 @@
* devices rather than being a per-browser quirk. * devices rather than being a per-browser quirk.
*/ */
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Check, Monitor, Moon, Sun, Terminal } from 'lucide-react'; import { Check, LogOut, Monitor, Moon, Sun, Terminal } from 'lucide-react';
import { get, patch } from '@/lib/api'; import { get, getSupabase, patch } from '@/lib/api';
import { useTheme } from '@/lib/theme'; import { useTheme } from '@/lib/theme';
import { getAccent, THEME_MODES, type ThemeMode } from '@pig/core'; import { getAccent, THEME_MODES, type ThemeMode } from '@pig/core';
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input } from '@/components/ui'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input } from '@/components/ui';
import { useState } from 'react'; import { useState } from 'react';
import { usePageTitle } from '@/lib/title';
interface Me { interface Me {
id: string; id: string;
@@ -23,6 +24,7 @@ interface Me {
} }
export function Settings() { export function Settings() {
usePageTitle('Settings');
const { data: me } = useQuery({ queryKey: ['me'], queryFn: () => get<Me>('/api/me') }); const { data: me } = useQuery({ queryKey: ['me'], queryFn: () => get<Me>('/api/me') });
return ( return (
@@ -34,6 +36,7 @@ export function Settings() {
<Appearance /> <Appearance />
<Profile me={me} /> <Profile me={me} />
<ConnectAgent /> <ConnectAgent />
<SessionCard />
</div> </div>
); );
} }
@@ -236,3 +239,47 @@ claude mcp add pig -- npx -y @pig/mcp`}</code>
</Card> </Card>
); );
} }
/**
* Signing out.
*
* There was no way to do this at all until now, which is an easy omission to
* make when developing signed in and a genuinely stranded feeling for anyone
* on a shared machine.
*
* Clearing the identity provider's session is enough: PIG holds no session of
* its own, and the auth listener in App.tsx notices and returns to sign-in
* without a reload.
*/
function SessionCard() {
const [busy, setBusy] = useState(false);
async function signOut() {
setBusy(true);
try {
await getSupabase()?.auth.signOut();
} finally {
// Belt and braces: if the provider call fails, a reload still lands on
// the sign-in screen rather than leaving a half-signed-out interface.
window.location.href = '/';
}
}
return (
<Card>
<CardHeader>
<CardTitle className="text-base">Session</CardTitle>
<p className="text-sm text-muted">
Signing out clears this browser only. API keys you have issued keep working
revoke those separately.
</p>
</CardHeader>
<CardContent>
<Button variant="outline" onClick={signOut} disabled={busy}>
<LogOut className="h-4 w-4" aria-hidden />
{busy ? 'Signing out…' : 'Sign out'}
</Button>
</CardContent>
</Card>
);
}