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:
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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('');
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user