mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(onboarding): never switch a module off on a record that already uses it
The derivation wrote an explicit off for every module the answers did not name, which is right for a record being set up and wrong for one with years of rows behind it. An established self-hoster has, for most modules, no stored preference at all — they were default-on and nobody ever touched the toggle — so the first confirm took away the navigation entry, the dashboard widget and the settings entry for domains full of content. The rows survived; the surfaces did not. A derived off now asks first, and only for the modules it would actually retract. Ownership is not re-answered: the measurement domains come from the same map the MCP reads and the coach snapshot gates off, and each remaining module names the one table its content lives in, exhaustively, so a module added later fails to compile until somebody says where its content is. A module whose domain holds rows keeps whatever the record already had, because absence is what "on" looks like in the stored map.
This commit is contained in:
1 parent
cc6e6ca221
commit
2d2350252f
6 files changed
+372
-5
No files matched your search
@@ -6,9 +6,11 @@ import { setOnboardingPendingCookie } from "@/lib/auth/session";
|
||||
import { invalidateUserHealthScore } from "@/lib/cache/invalidate";
|
||||
import { getOrCreateCycleProfile } from "@/lib/cycle/profile";
|
||||
import { normalisePrefs } from "@/lib/modules/gate";
|
||||
import { modulesHoldingRecordData } from "@/lib/modules/domain-data";
|
||||
import {
|
||||
deriveOnboardingModuleDefaults,
|
||||
mergeDerivedModulePreferences,
|
||||
OWNED_MODULE_KEYS,
|
||||
} from "@/lib/modules/registry";
|
||||
import {
|
||||
everyOnboardingQuestionSettled,
|
||||
@@ -148,6 +150,7 @@ async function completeNeedsFlow(userId: string, held: HeldUnitPreferences) {
|
||||
}
|
||||
|
||||
let derived = false;
|
||||
let keptForData: string[] = [];
|
||||
if (record.modulesDerivedAt === null) {
|
||||
const defaults = deriveOnboardingModuleDefaults({
|
||||
recordTarget: state.needs.recordTarget,
|
||||
@@ -163,9 +166,22 @@ async function completeNeedsFlow(userId: string, held: HeldUnitPreferences) {
|
||||
where: { id: userId },
|
||||
select: { modulePreferencesJson: true },
|
||||
});
|
||||
// Only the modules the answers would switch OFF are worth asking about,
|
||||
// and a record that already holds rows in one of those domains keeps its
|
||||
// surface: the flow orders a record, and it cannot order away content
|
||||
// somebody already has.
|
||||
const wouldSwitchOff = OWNED_MODULE_KEYS.filter(
|
||||
(key) => defaults.preferences[key] === false,
|
||||
);
|
||||
const holdsData = await modulesHoldingRecordData(
|
||||
prisma,
|
||||
userId,
|
||||
wouldSwitchOff,
|
||||
);
|
||||
const merged = mergeDerivedModulePreferences(
|
||||
normalisePrefs(row?.modulePreferencesJson),
|
||||
defaults.preferences,
|
||||
holdsData,
|
||||
);
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
@@ -187,6 +203,7 @@ async function completeNeedsFlow(userId: string, held: HeldUnitPreferences) {
|
||||
// A module toggle adds or removes a Health Score pillar, so the cached
|
||||
// composite was computed from a composition that no longer holds.
|
||||
invalidateUserHealthScore(userId);
|
||||
keptForData = [...holdsData];
|
||||
derived = true;
|
||||
}
|
||||
|
||||
@@ -208,7 +225,10 @@ async function completeNeedsFlow(userId: string, held: HeldUnitPreferences) {
|
||||
|
||||
annotate({
|
||||
action: { name: "onboarding.needs.complete" },
|
||||
meta: { outcome: derived ? "derived" : "already_derived" },
|
||||
meta: {
|
||||
outcome: derived ? "derived" : "already_derived",
|
||||
keptForData: keptForData.length,
|
||||
},
|
||||
});
|
||||
|
||||
return toOnboardingStateDto(written, held);
|
||||
|
||||
Reference in new issue
Block a user