fix(deferred): close frontend UI/UX tails from the hygiene sweep
- player YB2: keepalive pagehide beacon (api.saveProgressBeacon) so the resume position isn't lost on F5/close within 5s of the last checkpoint (mirrors Plex). - player YB4: bound consecutive unplayable items in auto-advance so an auto-advancing/loop=all queue can't spin over a run of dead videos. - downloads B6 (client): localise structured quota/edit errors centrally in api.req() via localizeDetail() + Intl.NumberFormat (fixes HU/DE + decimal sep); + 3-locale download error strings. - channels FB1: focusChannelToken bump so re-clicking the same channel re-seeds the search box; FB2: syncSubs invalidates feed/feed-count (set can change now). - config CB2: reseed the ConfigPanel draft on a key-set dataVersion + explicit post-save token, so a mid-edit refetch can't clobber an in-progress edit. - config AB3 (UI): a blank allow_empty field stores "" instead of resetting. - admin SB2/SC2: confirm + success toast on demo-whitelist remove and deny-invite (+ trilingual strings); SC1: disable only the acted-on row (pendingId), not all; SB3: pause the 1s scheduler countdown ticker while the tab is hidden. - messages CT4: only invalidate conversations/unread when the incoming-message count actually changed, not on every 20s poll; MB3 (client): react to the live unread ping (onUnread → invalidate); MC3: extract e2ee installKey (setup/unlock); CC2: hoist POLL_MS to messaging.ts; MB4: document the reload-scoped socket.
This commit is contained in:
@@ -38,15 +38,23 @@ export default function ConfigPanel() {
|
||||
// clearing the field, since an empty secret field means "leave unchanged").
|
||||
const [secretReset, setSecretReset] = useState<Record<string, boolean>>({});
|
||||
const [saveState, setSaveState] = useState<SaveState>("idle");
|
||||
// Bumped after our own save so the draft re-seeds from fresh server values; a plain mid-edit
|
||||
// refetch does NOT (see the effect below).
|
||||
const [reseedToken, setReseedToken] = useState(0);
|
||||
// One tab per config group (persisted). Called before the loading guard so hook order is
|
||||
// stable; the active id is clamped to a real group at render time once data has loaded.
|
||||
const [tab, setTab] = usePersistedTab(LS.configTab);
|
||||
|
||||
// Re-seed the draft whenever the server data changes (initial load + after a save/reset).
|
||||
// A stable signature of the config SHAPE — changes only when keys are added/removed, not when an
|
||||
// unchanged refetch produces a new baseline object. Re-seed the draft on a shape change (initial
|
||||
// load, registry change) or an explicit post-save bump — so a refetch that resolves mid-edit
|
||||
// (e.g. an invalidation) can no longer clobber the admin's in-progress draft (CB2).
|
||||
const dataVersion = useMemo(() => items.map((i) => i.key).join("|"), [items]);
|
||||
useEffect(() => {
|
||||
setDraft(baseline);
|
||||
setSecretReset({});
|
||||
}, [baseline]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataVersion, reseedToken]);
|
||||
|
||||
const isDirty = (it: ConfigItem): boolean => {
|
||||
if (it.secret) return (draft[it.key] ?? "").trim() !== "" || !!secretReset[it.key];
|
||||
@@ -67,12 +75,16 @@ export default function ConfigPanel() {
|
||||
} else if (it.type === "bool") {
|
||||
await api.setConfig(key, raw === "true");
|
||||
} else if (raw === "") {
|
||||
if (it.is_set) await api.resetConfig(key); // cleared → fall back to default
|
||||
// allow_empty keys treat a blank as an explicit empty value (e.g. disable smtp_user /
|
||||
// youtube_api_proxy) rather than resetting to the env default (AB3).
|
||||
if (it.allow_empty) await api.setConfig(key, "");
|
||||
else if (it.is_set) await api.resetConfig(key); // cleared → fall back to default
|
||||
} else {
|
||||
await api.setConfig(key, it.type === "int" ? Number(raw) : raw);
|
||||
}
|
||||
}
|
||||
await qc.invalidateQueries({ queryKey: ["admin-config"] });
|
||||
setReseedToken((n) => n + 1); // re-seed the draft from the saved server state
|
||||
setSaveState("saved");
|
||||
setTimeout(() => setSaveState((s) => (s === "saved" ? "idle" : s)), 2500);
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user