mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
fix(modules): switch the inbound-documents module off in code
The inbound-documents surface is withdrawn from this release and returns later with more depth. Park its module key (`inboundDocuments`) in a new `CODE_DISABLED_MODULE_KEYS` list so it is hard-off for every account ahead of both the operator-availability and the per-user opt-in layer. The gate short-circuits a code-disabled key to disabled, so the resolved module map reports it off everywhere: the nav entry stays born-gated and hidden, the `/documents` page redirects, and every `/api/documents/inbound/*` route returns the 403 module-disabled envelope. The Modules hub and the operator-availability panel drop the row, and the per-user PATCH surface refuses to write the key (a clean 422 instead of an inert preference). The whole implementation stays intact — routes, the inbound-documents view, the registry entry with its opt-in marker, the validation schemas, and the migration are all preserved. Re-enabling the module is a one-line removal from the list once the rebuild lands. Regenerate the OpenAPI contract: the module-preferences PATCH body no longer advertises the parked key.
This commit is contained in:
1 parent
4266203d3a
commit
7e83af5701
8 files changed
+152
-73
No files matched your search
@@ -7,7 +7,11 @@ import { toast } from "sonner";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MODULE_KEYS, MODULE_REGISTRY } from "@/lib/modules/registry";
|
||||
import {
|
||||
MODULE_KEYS,
|
||||
MODULE_REGISTRY,
|
||||
isCodeDisabledModule,
|
||||
} from "@/lib/modules/registry";
|
||||
import type { ModuleKey } from "@/lib/modules/registry";
|
||||
import { SettingsToggle } from "./_shared";
|
||||
import { apiGet, apiPatch } from "@/lib/api/api-fetch";
|
||||
@@ -90,24 +94,30 @@ export function ModuleAvailabilitySection() {
|
||||
description={t("admin.modules.description")}
|
||||
/>
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{MODULE_KEYS.map((key) => {
|
||||
const def = MODULE_REGISTRY[key];
|
||||
// Default-available until the read resolves; an explicit `false`
|
||||
// from the operator layer is the only thing that turns it off.
|
||||
const available = availability?.[key] ?? true;
|
||||
return (
|
||||
<SettingsToggle
|
||||
key={key}
|
||||
label={t(def.labelKey)}
|
||||
description={t(def.descriptionKey)}
|
||||
checked={available}
|
||||
onCheckedChange={(checked) =>
|
||||
mutation.mutate({ [key]: checked } as Partial<AvailabilityMap>)
|
||||
}
|
||||
disabled={mutation.isPending}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{MODULE_KEYS
|
||||
// A module switched off in code (pending a rebuild) is hard-off
|
||||
// server-wide; drop its row so the operator toggle can't mislead.
|
||||
.filter((key) => !isCodeDisabledModule(key))
|
||||
.map((key) => {
|
||||
const def = MODULE_REGISTRY[key];
|
||||
// Default-available until the read resolves; an explicit `false`
|
||||
// from the operator layer is the only thing that turns it off.
|
||||
const available = availability?.[key] ?? true;
|
||||
return (
|
||||
<SettingsToggle
|
||||
key={key}
|
||||
label={t(def.labelKey)}
|
||||
description={t(def.descriptionKey)}
|
||||
checked={available}
|
||||
onCheckedChange={(checked) =>
|
||||
mutation.mutate({
|
||||
[key]: checked,
|
||||
} as Partial<AvailabilityMap>)
|
||||
}
|
||||
disabled={mutation.isPending}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,11 @@ import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { AuthUser } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MODULE_KEYS, moduleDelegatesTo } from "@/lib/modules/registry";
|
||||
import {
|
||||
MODULE_KEYS,
|
||||
isCodeDisabledModule,
|
||||
moduleDelegatesTo,
|
||||
} from "@/lib/modules/registry";
|
||||
|
||||
// Capture the latest useMutation config + a shared mutate spy so we can
|
||||
// invoke `mutationFn` / `onSuccess` by hand (the SSR pass can't fire a
|
||||
@@ -112,7 +116,13 @@ describe("<ModulesSection>", () => {
|
||||
// only renders the toggleable modules.
|
||||
const html = render();
|
||||
for (const key of MODULE_KEYS) {
|
||||
if (moduleDelegatesTo(key) !== undefined) {
|
||||
if (isCodeDisabledModule(key)) {
|
||||
// v1.25.3 — a module switched off in code (pending a rebuild) drops
|
||||
// out of the hub entirely: no Switch, no deep-link, no row.
|
||||
expect(html, `code-disabled has no switch ${key}`).not.toContain(
|
||||
`id="module-toggle-${key}"`,
|
||||
);
|
||||
} else if (moduleDelegatesTo(key) !== undefined) {
|
||||
// Delegated modules (cycle/coach) are owned by their real control
|
||||
// elsewhere — they render as a read-only "manage in X" deep-link,
|
||||
// never a live Switch that would write an inert disabled-allowlist
|
||||
|
||||
@@ -55,6 +55,7 @@ import { apiPatch } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
MODULE_REGISTRY,
|
||||
isCodeDisabledModule,
|
||||
moduleDelegatesTo,
|
||||
type ModuleKey,
|
||||
} from "@/lib/modules/registry";
|
||||
@@ -130,48 +131,52 @@ export function ModulesSection() {
|
||||
{t("settings.sections.modules.toggleable.description")}
|
||||
</p>
|
||||
<div className="divide-border divide-y pl-7">
|
||||
{(Object.keys(MODULE_REGISTRY) as ModuleKey[]).map((key) => {
|
||||
const def = MODULE_REGISTRY[key];
|
||||
// Default-on: a module is enabled unless explicitly `false`.
|
||||
const enabled = modules[key] !== false;
|
||||
// Delegated modules (cycle/coach) are owned elsewhere — never a
|
||||
// live toggle here; deep-link to their real control instead.
|
||||
const delegated = moduleDelegatesTo(key) !== undefined;
|
||||
// Operator turned this module off server-wide: no per-user
|
||||
// toggle can re-enable it, so show a read-only note. Delegated
|
||||
// rows already deep-link, so this only matters for owned keys.
|
||||
const operatorDisabled =
|
||||
!delegated && moduleAvailability[key] === false;
|
||||
return (
|
||||
<ModuleToggleRow
|
||||
key={key}
|
||||
moduleKey={key}
|
||||
icon={MODULE_ICONS[key]}
|
||||
label={t(def.labelKey)}
|
||||
description={t(def.descriptionKey)}
|
||||
enabled={enabled}
|
||||
pending={toggle.isPending}
|
||||
managedAt={
|
||||
delegated && def.managedAt
|
||||
? {
|
||||
href: def.managedAt.href,
|
||||
label: t(def.managedAt.labelKey),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
manageLinkLabel={
|
||||
delegated && def.managedAt
|
||||
? t("settings.sections.modules.manageIn", {
|
||||
section: t(def.managedAt.labelKey),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
operatorDisabled={operatorDisabled}
|
||||
operatorNote={t("settings.sections.modules.operatorDisabled")}
|
||||
onToggle={(next) => toggle.mutate({ key, enabled: next })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{(Object.keys(MODULE_REGISTRY) as ModuleKey[])
|
||||
// Modules switched off in code (pending a rebuild) carry no live
|
||||
// toggle — drop the row entirely so a user can't turn one on.
|
||||
.filter((key) => !isCodeDisabledModule(key))
|
||||
.map((key) => {
|
||||
const def = MODULE_REGISTRY[key];
|
||||
// Default-on: a module is enabled unless explicitly `false`.
|
||||
const enabled = modules[key] !== false;
|
||||
// Delegated modules (cycle/coach) are owned elsewhere — never a
|
||||
// live toggle here; deep-link to their real control instead.
|
||||
const delegated = moduleDelegatesTo(key) !== undefined;
|
||||
// Operator turned this module off server-wide: no per-user
|
||||
// toggle can re-enable it, so show a read-only note. Delegated
|
||||
// rows already deep-link, so this only matters for owned keys.
|
||||
const operatorDisabled =
|
||||
!delegated && moduleAvailability[key] === false;
|
||||
return (
|
||||
<ModuleToggleRow
|
||||
key={key}
|
||||
moduleKey={key}
|
||||
icon={MODULE_ICONS[key]}
|
||||
label={t(def.labelKey)}
|
||||
description={t(def.descriptionKey)}
|
||||
enabled={enabled}
|
||||
pending={toggle.isPending}
|
||||
managedAt={
|
||||
delegated && def.managedAt
|
||||
? {
|
||||
href: def.managedAt.href,
|
||||
label: t(def.managedAt.labelKey),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
manageLinkLabel={
|
||||
delegated && def.managedAt
|
||||
? t("settings.sections.modules.manageIn", {
|
||||
section: t(def.managedAt.labelKey),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
operatorDisabled={operatorDisabled}
|
||||
operatorNote={t("settings.sections.modules.operatorDisabled")}
|
||||
onToggle={(next) => toggle.mutate({ key, enabled: next })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user