fix(deferred): address /code-review findings on the closeout diff

- channels CB3: sort discovery rows NULLs-last (title is None) to match the DB
  ORDER BY exactly — coercing NULL title to "" floated untitled channels to the top.
- AdminUsers SC1: track in-flight rows in a Set, not a single id — per-row disabling
  now lets the admin start concurrent row actions, and a shared id was cleared by
  whichever settled first (re-enabling a still-pending row → possible double-submit).
- ChatThread CT4: reset the incoming-count ref when partnerId changes — the Messages
  page reuses one ChatThread across conversation switches, so a same-count switch
  could skip the open-marks-read badge refresh.
- messages MB3: push a bare {type:"unread"} (drop the now-dead count query — the
  client re-fetches on the signal and ignored the number anyway).
- api.ts: extract the shared keepalive `beacon()` helper (saveProgressBeacon +
  plexProgressBeacon were near-verbatim copies).
This commit is contained in:
npeter83 2026-07-12 06:21:29 +02:00
parent f0198f2e0a
commit 3394dabbeb
5 changed files with 72 additions and 64 deletions

View file

@ -299,6 +299,29 @@ const setupHeaders = (token: string) => ({
const ACTIVE_ACCOUNT_HEADER = "X-Siftlode-Account";
export const getActiveAccount = activeAccountId;
// Fire-and-forget POST that survives page unload (keepalive) — shared by the resume-position
// beacons (YT + Plex players). A normal fetch is cancelled on unload and React effect cleanup
// doesn't run on a full reload, so without keepalive the last position (esp. right after a seek)
// would be lost.
function beacon(url: string, body: Record<string, unknown>): void {
const active = getActiveAccount();
try {
void fetch(url, {
method: "POST",
keepalive: true,
credentials: "include",
headers: {
"Content-Type": "application/json",
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
},
body: JSON.stringify(body),
}).catch(() => {});
} catch {
/* ignore */
}
}
export function setActiveAccount(id: number): void {
try {
sessionStorage.setItem(ACTIVE_ACCOUNT_KEY, String(id));
@ -1102,30 +1125,12 @@ export const api = {
},
{ idempotent: true }
),
// Fire-and-forget YT progress save that survives page unload (F5/close/navigate) — mirrors
// plexProgressBeacon. A normal fetch is cancelled on unload and React effect cleanup doesn't run
// on a full reload, so the last position (esp. right after a seek) would be lost; keepalive lets
// it complete.
saveProgressBeacon: (id: string, positionSeconds: number, durationSeconds: number): void => {
const active = getActiveAccount();
try {
void fetch(`/api/videos/${encodeURIComponent(id)}/progress`, {
method: "POST",
keepalive: true,
credentials: "include",
headers: {
"Content-Type": "application/json",
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
},
body: JSON.stringify({
position_seconds: Math.floor(positionSeconds),
duration_seconds: Math.floor(durationSeconds),
}),
}).catch(() => {});
} catch {
/* ignore */
}
},
// Fire-and-forget YT progress save that survives page unload (F5/close/navigate).
saveProgressBeacon: (id: string, positionSeconds: number, durationSeconds: number): void =>
beacon(`/api/videos/${encodeURIComponent(id)}/progress`, {
position_seconds: Math.floor(positionSeconds),
duration_seconds: Math.floor(durationSeconds),
}),
videoDetail: (id: string): Promise<VideoDetail> => req(`/api/videos/${id}`),
pauseSync: () => req("/api/sync/pause", { method: "POST" }),
resumeSync: () => req("/api/sync/resume", { method: "POST" }),
@ -1358,23 +1363,12 @@ export const api = {
position_seconds: number,
duration_seconds: number,
final = false,
): void => {
const active = getActiveAccount();
try {
void fetch(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
method: "POST",
keepalive: true,
credentials: "include",
headers: {
"Content-Type": "application/json",
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
},
body: JSON.stringify({ position_seconds, duration_seconds, final }),
}).catch(() => {});
} catch {
/* ignore */
}
},
): void =>
beacon(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
position_seconds,
duration_seconds,
final,
}),
plexSetState: (id: string, status: "new" | "watched" | "hidden"): Promise<unknown> =>
req(`/api/plex/item/${encodeURIComponent(id)}/state`, {
method: "POST",