Make mutations confirm themselves, and seed the evidence trail
CI / verify (push) Successful in 2m51s
CI / verify (push) Successful in 2m51s
Two demo gaps, both of which made working features look like they were not there. **Toasts fired into nothing.** RecordSheets already called toast.success on every save, but <Toaster /> was never mounted, so nothing appeared. It could not be mounted, either: the shadcn original imports next-themes, which PIG does not use — it has its own provider so a chosen theme is persisted server-side and follows a user between devices. Rewired to PIG's useTheme, mounted inside ThemeProvider, and offset clear of the phone tab bar and the home indicator. Feedback added where the interface otherwise gives none: allocation and hold report the GPU-hours actually written, because the sheet closes on success and the only other evidence is a number moving off-screen; releasing a hold says the capacity is sellable again; fact decisions say what the decision meant, and that approving evidence is not the same as writing it to a record; the profile form confirms rather than just clearing itself, which otherwise reads as the input being discarded. **The fact table was empty**, so the review queue and every provenance tooltip had nothing to show — the mechanism that makes an agent-written CRM trustworthy, invisible. Six agent-derived facts seeded with a deliberate mix: two applied, showing what a confident agent writes unprompted, and four proposed, including one weak claim that a reviewer should reject, so the queue is not a row of obvious approvals. Each carries a score, a band, evidence and where available a source. Idempotent on subject+field+value; verified over two runs. Verified: toast confirmed firing in a real browser on a 393px viewport, 135 unit tests and e2e green, typecheck clean, CSP hash unchanged, 0px horizontal overflow across 12 routes at both breakpoints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ import {
|
||||
} from '@/components/ui/sheet';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { ApiError, compactNumber, get, money, percent, post, shortDate } from '@/lib/api';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface AvailabilityRow {
|
||||
commitmentId: string;
|
||||
@@ -295,10 +296,24 @@ export function AllocationSheet({
|
||||
})
|
||||
: post<AllocationRecord>('/api/allocations', { ...body, status: values.status });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (allocation, values) => {
|
||||
await refresh();
|
||||
onOpenChange(false);
|
||||
// The sheet closes on success, so without this the only evidence the
|
||||
// write landed is a number moving somewhere off-screen. Report what
|
||||
// actually happened, in the units the seller was thinking in.
|
||||
const hours = Number(allocation.gpuHours ?? values.gpuHours).toLocaleString();
|
||||
toast.success(
|
||||
values.kind === 'hold' ? 'Capacity held' : 'Capacity allocated',
|
||||
{
|
||||
description:
|
||||
values.kind === 'hold'
|
||||
? `${hours} GPU-hours reserved. The hold releases automatically when it expires.`
|
||||
: `${hours} GPU-hours committed. Margin and utilisation have been updated.`,
|
||||
},
|
||||
);
|
||||
},
|
||||
onError: (error) => toast.error('Could not save', { description: errorMessage(error) }),
|
||||
});
|
||||
const release = useMutation({
|
||||
mutationFn: (id: string) =>
|
||||
@@ -306,8 +321,16 @@ export function AllocationSheet({
|
||||
reason: releaseReason.trim() || undefined,
|
||||
}),
|
||||
onMutate: () => setReleaseError(null),
|
||||
onSuccess: refresh,
|
||||
onError: (error) => setReleaseError(errorMessage(error)),
|
||||
onSuccess: async () => {
|
||||
await refresh();
|
||||
toast.success('Hold released', {
|
||||
description: 'The capacity is available to sell again.',
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
setReleaseError(errorMessage(error));
|
||||
toast.error('Could not release the hold', { description: errorMessage(error) });
|
||||
},
|
||||
});
|
||||
|
||||
const chooseCommitment = (id: string) => {
|
||||
|
||||
@@ -1,29 +1,43 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
/**
|
||||
* Toast host.
|
||||
*
|
||||
* The shadcn original reads the theme from `next-themes`, which PIG does not
|
||||
* use — it has its own provider so a user's choice can be persisted server-side
|
||||
* and follow them between devices. Importing next-themes here would have thrown
|
||||
* at module load, which is why the Toaster was never mounted and every mutation
|
||||
* in the app completed in silence.
|
||||
*
|
||||
* Rewired to PIG's `useTheme`, which already resolves `system` to a concrete
|
||||
* light or dark value.
|
||||
*/
|
||||
import { Toaster as Sonner, type ToasterProps } from 'sonner';
|
||||
import { useTheme } from '@/lib/theme';
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
export function Toaster(props: ToasterProps) {
|
||||
const { resolved } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme={resolved}
|
||||
className="toaster group"
|
||||
// Above the sheets and dialogs it confirms, and clear of the phone tab
|
||||
// bar and the home indicator beneath it.
|
||||
position="bottom-right"
|
||||
offset="1rem"
|
||||
style={{ marginBottom: 'calc(var(--safe-bottom) + 4rem)' }}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
'group toast group-[.toaster]:bg-surface group-[.toaster]:text-fg ' +
|
||||
'group-[.toaster]:border-border group-[.toaster]:shadow-lg',
|
||||
description: 'group-[.toast]:text-muted',
|
||||
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
|
||||
cancelButton: 'group-[.toast]:bg-surface-2 group-[.toast]:text-muted',
|
||||
error: 'group-[.toaster]:text-danger',
|
||||
success: 'group-[.toaster]:text-positive',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
|
||||
Reference in New Issue
Block a user