Skip to content

Commit 9a2bf92

Browse files
committed
Added Diagnostics Orphaned Cleanup button
1 parent 231a1b9 commit 9a2bf92

5 files changed

Lines changed: 401 additions & 45 deletions

File tree

src/routes/(protected)/dynamic-entities/diagnostics/+page.svelte

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,36 @@
99
1010
let searchQuery = $state("");
1111
let copiedId = $state<string | null>(null);
12+
let cleaningUp = $state(false);
13+
let cleanupResult = $state<{ deleted_orphaned_entities: any[]; total_records_deleted: number } | null>(null);
14+
let cleanupError = $state<string | null>(null);
15+
let showCleanupConfirm = $state(false);
16+
17+
async function handleCleanup() {
18+
showCleanupConfirm = false;
19+
cleaningUp = true;
20+
cleanupError = null;
21+
cleanupResult = null;
22+
23+
try {
24+
const response = await fetch("/api/dynamic-entities/diagnostics/cleanup", {
25+
method: "DELETE",
26+
});
27+
28+
const data = await response.json();
29+
30+
if (!response.ok) {
31+
cleanupError = data.error || "Failed to clean up orphaned records";
32+
return;
33+
}
34+
35+
cleanupResult = data;
36+
} catch (err: any) {
37+
cleanupError = err?.message || "Failed to clean up orphaned records";
38+
} finally {
39+
cleaningUp = false;
40+
}
41+
}
1242
1343
const filteredDiagnostics = $derived(
1444
diagnostics.filter((diag: any) => {
@@ -203,6 +233,99 @@ ${diag.triedKeys ? `Tried Keys: ${diag.triedKeys.join(", ")}` : ""}
203233
</tbody>
204234
</table>
205235
</div>
236+
237+
<!-- Cleanup Button -->
238+
<div class="border-t border-yellow-300 p-4 dark:border-yellow-700">
239+
{#if showCleanupConfirm}
240+
<div class="rounded-lg border border-red-300 bg-red-50 p-4 dark:border-red-700 dark:bg-red-900/20">
241+
<p class="mb-3 text-sm font-medium text-red-800 dark:text-red-200">
242+
Are you sure you want to delete all orphaned records? This will permanently remove {orphanedEntities.reduce((sum: number, o: any) => sum + (o.record_count || 0), 0)} record(s) across {orphanedEntities.length} orphaned entit{orphanedEntities.length === 1 ? 'y' : 'ies'}.
243+
</p>
244+
<div class="flex gap-2">
245+
<button
246+
type="button"
247+
onclick={handleCleanup}
248+
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700"
249+
>
250+
Yes, Delete Orphaned Records
251+
</button>
252+
<button
253+
type="button"
254+
onclick={() => showCleanupConfirm = false}
255+
class="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300 dark:bg-gray-600 dark:text-gray-200 dark:hover:bg-gray-500"
256+
>
257+
Cancel
258+
</button>
259+
</div>
260+
</div>
261+
{:else}
262+
<button
263+
type="button"
264+
onclick={() => showCleanupConfirm = true}
265+
disabled={cleaningUp}
266+
class="inline-flex items-center gap-2 rounded-lg bg-red-100 px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-200 disabled:opacity-50 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50"
267+
>
268+
{#if cleaningUp}
269+
<svg class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
270+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
271+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
272+
</svg>
273+
Cleaning up...
274+
{:else}
275+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
276+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
277+
</svg>
278+
Clean Up Orphaned Records
279+
{/if}
280+
</button>
281+
{/if}
282+
283+
<!-- Cleanup Result -->
284+
{#if cleanupResult}
285+
<div class="mt-4 rounded-lg border border-green-300 bg-green-50 p-4 dark:border-green-700 dark:bg-green-900/20">
286+
<div class="flex items-center gap-2 mb-2">
287+
<svg class="h-5 w-5 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
288+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
289+
</svg>
290+
<p class="text-sm font-semibold text-green-800 dark:text-green-200">
291+
Cleanup complete: {cleanupResult.total_records_deleted} record(s) deleted
292+
</p>
293+
</div>
294+
{#if cleanupResult.deleted_orphaned_entities && cleanupResult.deleted_orphaned_entities.length > 0}
295+
<div class="mt-2 overflow-x-auto">
296+
<table class="min-w-full divide-y divide-green-300 dark:divide-green-700">
297+
<thead>
298+
<tr>
299+
<th class="px-3 py-1.5 text-left text-xs font-medium uppercase text-green-700 dark:text-green-300">Entity Name</th>
300+
<th class="px-3 py-1.5 text-right text-xs font-medium uppercase text-green-700 dark:text-green-300">Records Deleted</th>
301+
</tr>
302+
</thead>
303+
<tbody class="divide-y divide-green-200 dark:divide-green-800">
304+
{#each cleanupResult.deleted_orphaned_entities as deleted}
305+
<tr>
306+
<td class="whitespace-nowrap px-3 py-2 text-sm font-mono text-green-900 dark:text-green-100">{deleted.entity_name}</td>
307+
<td class="whitespace-nowrap px-3 py-2 text-right text-sm font-mono text-green-900 dark:text-green-100">{deleted.records_deleted}</td>
308+
</tr>
309+
{/each}
310+
</tbody>
311+
</table>
312+
</div>
313+
{/if}
314+
</div>
315+
{/if}
316+
317+
<!-- Cleanup Error -->
318+
{#if cleanupError}
319+
<div class="mt-4 rounded-lg border border-red-300 bg-red-50 p-4 dark:border-red-700 dark:bg-red-900/20">
320+
<div class="flex items-center gap-2">
321+
<svg class="h-5 w-5 text-red-600 dark:text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
322+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
323+
</svg>
324+
<p class="text-sm font-medium text-red-800 dark:text-red-200">{cleanupError}</p>
325+
</div>
326+
</div>
327+
{/if}
328+
</div>
206329
</div>
207330
</div>
208331
{:else}

src/routes/(protected)/dynamic-entities/system/+page.svelte

Lines changed: 95 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,36 @@
9494
}
9595
}
9696
97+
let backingUp = $state<Record<string, boolean>>({});
98+
99+
async function backupEntity(entityId: string, entityName: string) {
100+
if (backingUp[entityId]) return;
101+
102+
backingUp[entityId] = true;
103+
104+
try {
105+
const response = await fetch(`/api/dynamic-entities/${entityId}/backup`, {
106+
method: "POST",
107+
});
108+
109+
const responseData = await response.json();
110+
111+
if (!response.ok) {
112+
throw new Error(responseData.error || "Failed to backup entity");
113+
}
114+
115+
alert(`Backup created: ${responseData.entity_name}`);
116+
window.location.reload();
117+
} catch (error) {
118+
const errorMsg =
119+
error instanceof Error ? error.message : "Failed to backup entity";
120+
alert(`Error: ${errorMsg}`);
121+
console.error("Backup error:", error);
122+
} finally {
123+
backingUp[entityId] = false;
124+
}
125+
}
126+
97127
function getPropertyCount(entity: any): number {
98128
const schema = getSchema(entity);
99129
return schema?.properties ? Object.keys(schema.properties).length : 0;
@@ -355,37 +385,32 @@
355385
<thead class="bg-gray-50 dark:bg-gray-900">
356386
<tr>
357387
<th
358-
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
388+
class="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
359389
>
360-
Entity Name
390+
Name
361391
</th>
362392
<th
363-
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
393+
class="px-3 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
364394
>
365395
Description
366396
</th>
367397
<th
368-
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
398+
class="px-3 py-3 text-center text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
369399
>
370-
Properties
400+
Props
371401
</th>
372402
<th
373-
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
403+
class="px-3 py-3 text-center text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
374404
>
375-
Required Fields
405+
Req
376406
</th>
377407
<th
378-
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
408+
class="px-3 py-3 text-center text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
379409
>
380410
Records
381411
</th>
382412
<th
383-
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
384-
>
385-
Personal
386-
</th>
387-
<th
388-
class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
413+
class="px-3 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
389414
>
390415
Actions
391416
</th>
@@ -397,7 +422,7 @@
397422
{#each filteredEntities as entity}
398423
{@const schema = getSchema(entity)}
399424
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
400-
<td class="whitespace-nowrap px-6 py-4 text-sm font-medium">
425+
<td class="whitespace-nowrap px-3 py-3 text-sm font-medium">
401426
<a
402427
href="/dynamic-entities/system/{entity.dynamic_entity_id}"
403428
class="text-blue-600 hover:text-blue-800 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
@@ -406,49 +431,27 @@
406431
</a>
407432
</td>
408433
<td
409-
class="max-w-md px-6 py-4 text-sm text-gray-700 dark:text-gray-300"
434+
class="max-w-xs truncate px-3 py-3 text-sm text-gray-700 dark:text-gray-300"
435+
title={schema?.description || ""}
410436
>
411437
{schema?.description || "No description"}
412438
</td>
413439
<td
414-
class="whitespace-nowrap px-6 py-4 text-sm text-gray-700 dark:text-gray-300"
440+
class="whitespace-nowrap px-3 py-3 text-center text-sm text-gray-700 dark:text-gray-300"
415441
>
416-
<span
417-
class="inline-flex items-center rounded-full bg-blue-100 px-2.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200"
418-
>
419-
{getPropertyCount(entity)}
420-
</span>
442+
{getPropertyCount(entity)}
421443
</td>
422444
<td
423-
class="whitespace-nowrap px-6 py-4 text-sm text-gray-700 dark:text-gray-300"
445+
class="whitespace-nowrap px-3 py-3 text-center text-sm text-gray-700 dark:text-gray-300"
424446
>
425-
<span
426-
class="inline-flex items-center rounded-full bg-purple-100 px-2.5 py-0.5 text-xs font-medium text-purple-800 dark:bg-purple-900 dark:text-purple-200"
427-
>
428-
{getRequiredFieldsCount(entity)}
429-
</span>
447+
{getRequiredFieldsCount(entity)}
430448
</td>
431-
<td class="whitespace-nowrap px-6 py-4 text-sm">
449+
<td class="whitespace-nowrap px-3 py-3 text-center text-sm">
432450
<span class="font-medium text-gray-900 dark:text-gray-100">
433451
{entity.record_count}
434452
</span>
435453
</td>
436-
<td class="whitespace-nowrap px-6 py-4 text-sm">
437-
{#if entity.has_personal_entity}
438-
<span
439-
class="inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900 dark:text-green-200"
440-
>
441-
Yes
442-
</span>
443-
{:else}
444-
<span
445-
class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-200"
446-
>
447-
No
448-
</span>
449-
{/if}
450-
</td>
451-
<td class="whitespace-nowrap px-6 py-4 text-right text-sm">
454+
<td class="whitespace-nowrap px-3 py-3 text-right text-sm">
452455
<div class="flex justify-end gap-2">
453456
<button
454457
type="button"
@@ -479,6 +482,53 @@
479482
/>
480483
</svg>
481484
</button>
485+
<button
486+
type="button"
487+
onclick={() =>
488+
backupEntity(
489+
entity.dynamic_entity_id,
490+
getEntityName(entity),
491+
)}
492+
disabled={backingUp[entity.dynamic_entity_id]}
493+
class="text-green-600 hover:text-green-900 disabled:opacity-50 dark:text-green-400 dark:hover:text-green-300"
494+
title="Backup Entity (definition + data)"
495+
>
496+
{#if backingUp[entity.dynamic_entity_id]}
497+
<svg
498+
class="h-5 w-5 animate-spin"
499+
fill="none"
500+
viewBox="0 0 24 24"
501+
>
502+
<circle
503+
class="opacity-25"
504+
cx="12"
505+
cy="12"
506+
r="10"
507+
stroke="currentColor"
508+
stroke-width="4"
509+
/>
510+
<path
511+
class="opacity-75"
512+
fill="currentColor"
513+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
514+
/>
515+
</svg>
516+
{:else}
517+
<svg
518+
class="h-5 w-5"
519+
fill="none"
520+
stroke="currentColor"
521+
viewBox="0 0 24 24"
522+
>
523+
<path
524+
stroke-linecap="round"
525+
stroke-linejoin="round"
526+
stroke-width="2"
527+
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
528+
/>
529+
</svg>
530+
{/if}
531+
</button>
482532
<button
483533
type="button"
484534
onclick={() =>

0 commit comments

Comments
 (0)