feat(clients): orphan cleanup + export/import via CodeMirror modals

Add three client-management actions to the Clients page More menu:

- Delete unattached clients: removes every client with no inbound
  attachment, cascading its traffic rows, IP log, and external links
  (POST /clients/delOrphans).
- Export clients: shows the {client, inboundIds} list in a read-only
  CodeMirror viewer with copy/download (GET /clients/export returns the
  array in the standard envelope).
- Import clients: pastes that JSON into an editable CodeMirror editor,
  mirroring Import an Inbound (POST /clients/import takes a { data }
  body). Attached clients go through the create-and-attach path; items
  with no inboundIds are restored as bare records; existing emails are
  never overwritten and are reported as skipped.

Document the new endpoints in api-docs and translate the new strings
into all supported languages.
This commit is contained in:
MHSanaei
2026-06-21 23:06:10 +02:00
parent 0483273839
commit 0b0b6250d6
19 changed files with 736 additions and 14 deletions
+28
View File
@@ -402,6 +402,22 @@ export function useClients() {
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
});
const delOrphansMut = useMutation({
mutationFn: async () => {
const raw = await HttpUtil.post('/panel/api/clients/delOrphans');
return parseMsg(raw, DelDepletedResultSchema, 'clients/delOrphans');
},
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
});
const importClientsMut = useMutation({
mutationFn: async (data: string): Promise<Msg<BulkCreateResult>> => {
const raw = await HttpUtil.post('/panel/api/clients/import', { data }, JSON_HEADERS);
return parseMsg(raw, BulkCreateResultSchema, 'clients/import');
},
onSuccess: (msg) => { if (msg?.success) invalidateAll(); },
});
const create = useCallback((payload: unknown) => createMut.mutateAsync(payload), [createMut]);
const update = useCallback((email: string, client: unknown) => {
if (!email) return Promise.resolve(null as unknown as Msg<unknown>);
@@ -459,6 +475,15 @@ export function useClients() {
}, [resetTrafficMut]);
const resetAllTraffics = useCallback(() => resetAllTrafficsMut.mutateAsync(), [resetAllTrafficsMut]);
const delDepleted = useCallback(() => delDepletedMut.mutateAsync(), [delDepletedMut]);
const delOrphans = useCallback(() => delOrphansMut.mutateAsync(), [delOrphansMut]);
const importClients = useCallback((data: string) => importClientsMut.mutateAsync(data), [importClientsMut]);
// Fetch the exported clients so the page can show them in a CodeMirror viewer
// (Copy / Download), rather than triggering an immediate browser download.
const exportClients = useCallback(async (): Promise<unknown[] | null> => {
const msg = await HttpUtil.get('/panel/api/clients/export');
if (!msg?.success) return null;
return Array.isArray(msg.obj) ? msg.obj : [];
}, []);
const setEnable = useCallback(async (client: ClientRecord, enable: boolean) => {
if (!client?.email) return null;
@@ -575,6 +600,9 @@ export function useClients() {
resetTraffic,
resetAllTraffics,
delDepleted,
delOrphans,
exportClients,
importClients,
setEnable,
applyTrafficEvent,
applyClientStatsEvent,