mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(auth): publish the resolved Coach reasoning on the account payload
GET /api/auth/me gains coachReasoning: the level a turn runs at, the person's own pick, the highest level the settings may offer, whether reasoning can run at all, whether the lowest option is a real off, and the layer that decided. Resolved on the server so the web settings and the native client read the same answer. The web reads it through useCoachReasoning. The Coach preferences route names the stored level on its wide event, and a unit test round-trips the level through the account-settings backup section.
This commit is contained in:
1 parent
adb4cd5c40
commit
df0689ffbe
10 files changed
+554
-3
No files matched your search
@@ -12,7 +12,11 @@ vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => ({ user: authUser }),
|
||||
}));
|
||||
|
||||
import { useAiCapability, useAiProviderState } from "../use-ai-capability";
|
||||
import {
|
||||
useAiCapability,
|
||||
useAiProviderState,
|
||||
useCoachReasoning,
|
||||
} from "../use-ai-capability";
|
||||
import type { AiCapabilityKey } from "@/lib/ai/capabilities/types";
|
||||
|
||||
function Probe({ capability }: { capability: AiCapabilityKey }) {
|
||||
@@ -107,3 +111,47 @@ describe("useAiCapability", () => {
|
||||
expect(render("briefing", false).state).toEqual(UNKNOWN);
|
||||
});
|
||||
});
|
||||
|
||||
function ReasoningProbe() {
|
||||
return <span>{JSON.stringify(useCoachReasoning())}</span>;
|
||||
}
|
||||
|
||||
function renderReasoning(withClient = true): unknown {
|
||||
const html = renderToStaticMarkup(
|
||||
withClient ? (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<ReasoningProbe />
|
||||
</QueryClientProvider>
|
||||
) : (
|
||||
<ReasoningProbe />
|
||||
),
|
||||
);
|
||||
return JSON.parse(
|
||||
html.replace(/^<span>|<\/span>$/g, "").replace(/"/g, '"'),
|
||||
);
|
||||
}
|
||||
|
||||
describe("useCoachReasoning", () => {
|
||||
const STATE = {
|
||||
level: "low",
|
||||
preference: "high",
|
||||
maxLevel: "low",
|
||||
available: true,
|
||||
offIsReal: true,
|
||||
source: "admin_cap",
|
||||
};
|
||||
|
||||
it("hands over the server's resolved block untouched", () => {
|
||||
authUser = { coachReasoning: STATE };
|
||||
expect(renderReasoning()).toEqual(STATE);
|
||||
});
|
||||
|
||||
it("is null while the payload is not there or carries no block", () => {
|
||||
authUser = null;
|
||||
expect(renderReasoning()).toBeNull();
|
||||
authUser = { coachReasoning: null };
|
||||
expect(renderReasoning()).toBeNull();
|
||||
authUser = { coachReasoning: STATE };
|
||||
expect(renderReasoning(false)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
AiProviderState,
|
||||
} from "@/lib/ai/capabilities/types";
|
||||
import { PROVIDER_DEFAULT_TIMEOUT_MS } from "@/lib/ai/effective-timeout";
|
||||
import type { CoachReasoningState } from "@/lib/ai/reasoning/resolve";
|
||||
|
||||
/**
|
||||
* The web reader for the `ai` block on `GET /api/auth/me`.
|
||||
@@ -66,6 +67,24 @@ function useAiProviderStateInner(): AiProviderState {
|
||||
return user?.ai?.provider ?? NO_PROVIDER_STATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.41 — the Coach's thinking depth, resolved on the server (`coachReasoning`
|
||||
* on `GET /api/auth/me`), or `null` while it is not known: `/me` loading, no
|
||||
* query client, or a server that does not publish it. Render the settings
|
||||
* from it; never recompute the cap or the switch.
|
||||
*/
|
||||
export function useCoachReasoning(): CoachReasoningState | null {
|
||||
const hasClient = useQueryClientMounted();
|
||||
if (!hasClient) return null;
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
return useCoachReasoningInner();
|
||||
}
|
||||
|
||||
function useCoachReasoningInner(): CoachReasoningState | null {
|
||||
const { user } = useAuth();
|
||||
return user?.coachReasoning ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The server's answer for one capability, or `null` while there is none yet
|
||||
* (`/me` loading, no signed-in account, no query client). For the few places
|
||||
|
||||
@@ -23,6 +23,7 @@ import { isDateFormatPreference, storeDateFormat } from "@/lib/date-format";
|
||||
import { storeTimezone } from "@/lib/timezone-mirror";
|
||||
import type { ModuleKey } from "@/lib/modules/registry";
|
||||
import type { AiCapabilities } from "@/lib/ai/capabilities/types";
|
||||
import type { CoachReasoningState } from "@/lib/ai/reasoning/resolve";
|
||||
import type { ModuleAccessState } from "@/lib/sharing/module-disclosure";
|
||||
import type { OnboardingStateDto } from "@/lib/onboarding/needs";
|
||||
import type { TourProgress } from "@/lib/onboarding/tour-progress";
|
||||
@@ -238,6 +239,13 @@ export interface AuthUser {
|
||||
* against a payload that does not carry a well-formed block.
|
||||
*/
|
||||
ai?: AiCapabilities | null;
|
||||
/**
|
||||
* v1.41 — the Coach's thinking depth, resolved on the server: the level a
|
||||
* turn runs at, the person's own pick, the highest option the operator
|
||||
* allows, and why. Read it through `useCoachReasoning`; `null` against a
|
||||
* server image that does not publish it.
|
||||
*/
|
||||
coachReasoning?: CoachReasoningState | null;
|
||||
/**
|
||||
* v1.36.0 — account sharing, resolved server-side. `accounts` is the
|
||||
* switcher's menu, `active` is the record this browser is inside (null when
|
||||
@@ -455,6 +463,12 @@ export async function fetchMe(): Promise<AuthUser> {
|
||||
typeof data.ai.provider === "object"
|
||||
? data.ai
|
||||
: null,
|
||||
// v1.41 — an older server image publishes no reasoning block; null, which
|
||||
// the hook reads as "not available here".
|
||||
coachReasoning:
|
||||
data.coachReasoning && typeof data.coachReasoning === "object"
|
||||
? data.coachReasoning
|
||||
: null,
|
||||
accountAccess,
|
||||
accountAccessStatus,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user