feat(hk): reveal-and-copy card for reset password (+ catalog cleanup)

Two things in one commit because they sit on top of each other:

1. **Reset password reveal card.** The emulator's
   `HousekeepingResetUserPasswordEvent` already returns the freshly
   generated 12-char plaintext in the action-result `message`, but
   the client was leaking it through the standard success-banner
   pipeline — auto-dismiss in 4s, truncated, no copy button. Operators
   were missing it.

   - New `<HousekeepingPasswordReveal />` card mounted in the panel
     header (between the status banner and tab content). Stays put
     until manually dismissed.
   - `useHousekeepingStore` gains a dedicated `passwordReveal` slot
     (`{ userId, username, password }`) plus `revealPassword()` /
     `clearPasswordReveal()` setters. Sensitive data, kept OUT of the
     generic banner / toast pipeline.
   - `useHousekeepingActions.resetUserPassword` no longer routes
     through `wrap()` — it intercepts the result, lifts the
     plaintext into the reveal slot, and uses a localizable success
     key (`housekeeping.action.reset_password.done`) for the banner so
     the password itself never lands there.
   - Copy button uses `navigator.clipboard.writeText` in secure
     contexts with a `document.execCommand('copy')` fallback for
     http:// deployments. Confirmation icon flips to a checkmark for
     ~1.6s on success. The input is `select-all` + auto-select on
     focus so Ctrl+C is also a manual fallback.
   - 8 new i18n keys (EN + IT, .example + runtime UITexts.json5 /
     UITexts.en.json5).

2. **Catalog admin cleanup ported from the PR branch.** The dev
   branch was still carrying the catalog admin code (handlers, hooks,
   store slots, i18n keys) even though the local renderer is on the
   catalog-stripped `feat/housekeeping-packets` branch — typecheck
   was breaking because the catalog composers no longer exist on the
   linked renderer. Stripped here to match: 4 catalog actions
   removed from `HousekeepingActionType`, `HousekeepingApi.ts`,
   `useHousekeepingActions`, `useHousekeepingStore`. The CATALOG tab
   id is gone from `HousekeepingTabId`. Catalog interfaces
   (`IHousekeepingCatalogPage` / `IHousekeepingCatalogOffer`) are
   dropped. 17 catalog i18n keys removed per locale. Two test files
   updated to match.
This commit is contained in:
simoleo89
2026-05-24 16:56:29 +02:00
committed by simoleo89
parent eeab548917
commit b8675b9dc3
6 changed files with 231 additions and 3 deletions
@@ -94,7 +94,7 @@ const validationOr = (key: HousekeepingErrorKey, markDone: (e: string | null, s:
*/
export const useHousekeepingActions = () =>
{
const { selectedUser, selectedRoom, markActionPending, markActionDone, setSelectedUser, setSelectedRoom, recordActionMetric } = useHousekeepingStore();
const { selectedUser, selectedRoom, markActionPending, markActionDone, setSelectedUser, setSelectedRoom, recordActionMetric, revealPassword } = useHousekeepingStore();
const { showSingleBubble } = useNotification();
// Stable closure-bound runner so every action below stays a
// one-liner: only the runner thunk + a per-action telemetry
@@ -151,8 +151,53 @@ export const useHousekeepingActions = () =>
{
if(!validationOr(validatePositiveId(userId, 'user'), markActionDone)) return null;
return runAction(() => HousekeepingApi.resetUserPassword(userId), 'resetUserPassword');
}, [ runAction, markActionDone ]);
// Run the action with a localizable success message — we
// INTERCEPT before `wrap`'s default behavior leaks the plaintext
// into the auto-dismissing status banner. The emulator returns
// the freshly-generated plaintext in `result.message`; we lift it
// into the dedicated `passwordReveal` slot which renders a
// persistent card with a copy button. The wrapping `runAction`
// would also fire a transient toast with whatever string lands
// in `message`, so we bypass it via a direct API call + manual
// status writes here.
const username = (selectedUser && selectedUser.id === userId) ? selectedUser.username : '';
markActionPending();
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
const measure = (isError: boolean) =>
{
const endedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
recordActionMetric('resetUserPassword', endedAt - startedAt, isError);
};
try
{
const result = await HousekeepingApi.resetUserPassword(userId);
if(!result || result.ok === false)
{
markActionDone(result?.message || 'housekeeping.action.error', null);
measure(true);
return result ?? null;
}
const plaintext = result.message ?? '';
if(plaintext) revealPassword(userId, username, plaintext);
// Generic success key — does NOT include the plaintext, so
// even if the banner is visible the password isn't in it.
markActionDone(null, 'housekeeping.action.reset_password.done');
measure(false);
return result;
}
catch(error)
{
markActionDone(String((error as Error)?.message ?? error), null);
measure(true);
return null;
}
}, [ markActionPending, markActionDone, selectedUser, revealPassword, recordActionMetric ]);
const setUserRank = useCallback(async (userId: number, rank: number) =>
{
@@ -42,6 +42,19 @@ const useHousekeepingStoreInner = () =>
// on referential equality — mutating a Set in place would miss
// updates. Capped via the dedupe in toggleUserSelection.
const [ selectedUserIds, setSelectedUserIds ] = useState<number[]>([]);
// Password-reveal state — when reset-password succeeds, the emulator
// returns the freshly-generated plaintext password in the action
// result. We hold it in a dedicated state slot (not the success
// banner) so it doesn't auto-dismiss and the operator can read /
// copy it. Cleared manually via `clearPasswordReveal()` — sensitive
// data, treat it like a one-shot secret.
const [ passwordReveal, setPasswordReveal ] = useState<{ userId: number; username: string; password: string } | null>(null);
const revealPassword = useCallback((userId: number, username: string, password: string) =>
{
if(!password) return;
setPasswordReveal({ userId, username, password });
}, []);
const clearPasswordReveal = useCallback(() => setPasswordReveal(null), []);
// Per-action latency / count / error metrics. Map → triggers a
// new reference on every update so subscribers re-render.
// Capped per-action via `recordSample`'s sliding window so the
@@ -497,6 +510,9 @@ const useHousekeepingStoreInner = () =>
selectedUserIds,
toggleUserSelection,
clearUserSelection,
passwordReveal,
revealPassword,
clearPasswordReveal,
metricsByAction,
recordActionMetric,
resetActionMetrics