Compare commits

...

54 commits

Author SHA1 Message Date
Classic298
f2faf56a3c
Merge 81c7617508 into ae47101dc6 2025-12-10 17:18:53 +01:00
Timothy Jaeryang Baek
ae47101dc6 refac 2025-12-10 11:07:41 -05:00
Classic298
81c7617508
feat: Make VACUUM database optimization optional (#36)
Co-authored-by: Claude <noreply@anthropic.com>
Fix #1: Remove duplicate scan in preview mode
Fix #2: Cache stat() result in audio cleanup
2025-11-13 20:45:47 +01:00
Classic298
a4ddb4b15b
fix (#35)
Co-authored-by: Claude <noreply@anthropic.com>
Fix #1: Remove duplicate scan in preview mode
Fix #2: Cache stat() result in audio cleanup
2025-11-13 19:29:02 +01:00
Classic298
c307d87262
sync (#34)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-13 19:13:21 +01:00
Classic298
20187f9a2d
fix file lock (#33)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-13 18:01:25 +01:00
Classic298
873b73e668
feat: Make VACUUM database optimization optional (#30)
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-11 19:39:20 +01:00
Classic298
60d7ad22ee
Claude/vacuum optional 011 c uw61vf5 s rym bh cw u1 ls w (#28)
PruneLock class
Vector cleanup error reporting
Lock acquisition/release
Optional VACUUM
Fixed folder deletion
2025-11-10 17:14:27 +01:00
Classic298
d94492dc0e
Merge branch 'dev' into universal_file_deletion 2025-11-10 15:34:05 +01:00
Classic298
195c3a57ae
Remove redundant parameter from delete_folder call 2025-11-10 15:33:17 +01:00
Classic298
2f53477de1
Merge branch 'open-webui:main' into universal_file_deletion 2025-10-14 13:50:02 +02:00
Classic298
1654b4f1bb
Merge branch 'dev' into universal_file_deletion 2025-09-29 22:21:34 +02:00
Classic298
a982bd73f7
Merge branch 'open-webui:main' into universal_file_deletion 2025-09-29 10:23:50 +02:00
Classic298
30095f5cb5
Merge branch 'open-webui:main' into universal_file_deletion 2025-09-26 10:41:46 +02:00
Classic298
f79a061113
Merge branch 'open-webui:main' into universal_file_deletion 2025-09-09 17:12:21 +02:00
Classic298
f263e750cf
Merge branch 'open-webui:main' into universal_file_deletion 2025-08-22 19:35:06 +02:00
Classic298
8156d0a30e
Update prune.py 2025-08-22 19:33:17 +02:00
Classic298
8231588eb4
pgvector 2025-08-22 19:17:52 +02:00
Classic298
46288924a2
Update prune.py 2025-08-22 18:29:26 +02:00
Classic298
155f53b867
Update prune.py 2025-08-22 18:17:24 +02:00
Classic298
bfa2eb631d
Update prune.py 2025-08-22 17:37:02 +02:00
Classic298
b5d93ae3db
Update prune.py 2025-08-22 17:02:36 +02:00
Classic298
4c7e6bd752
Update prune.py 2025-08-22 16:43:06 +02:00
Classic298
262848d647
Update prune.py 2025-08-22 16:39:47 +02:00
Classic298
13100ab9b3
Update Database.svelte 2025-08-22 16:33:04 +02:00
Classic298
2681fd268b
Update prune.py 2025-08-22 16:33:02 +02:00
Classic298
98650bd7d9
Update prune.ts 2025-08-22 16:29:51 +02:00
Classic298
f6c7c145a8
Update PruneDataDialog.svelte 2025-08-22 16:29:43 +02:00
Classic298
0230a1208b
Update Database.svelte 2025-08-22 16:29:31 +02:00
Classic298
bc19b51527
Update prune.py 2025-08-22 16:29:11 +02:00
Classic298
28f0079193
Update prune.py 2025-08-22 16:16:59 +02:00
Classic298
808fd0324d
Update prune.ts 2025-08-22 16:16:42 +02:00
Classic298
7abcc7bc59
Update Database.svelte 2025-08-22 16:16:31 +02:00
Classic298
544f8b72dc
Update PruneDataDialog.svelte 2025-08-22 16:16:14 +02:00
Classic298
5aa93ab97d
Update PruneDataDialog.svelte 2025-08-22 15:53:15 +02:00
Classic298
233167a041
Update PruneDataDialog.svelte 2025-08-22 15:49:55 +02:00
Classic298
74bfead38b
Update prune.py 2025-08-22 15:42:56 +02:00
Classic298
2ed95ef20e
Update PruneDataDialog.svelte 2025-08-22 15:42:05 +02:00
Classic298
daed47db03
Update Database.svelte 2025-08-22 15:38:53 +02:00
Classic298
4e6e5819a6
Update prune.ts 2025-08-22 15:38:41 +02:00
Classic298
596f02c2e9
Merge branch 'open-webui:main' into universal_file_deletion 2025-08-21 21:40:12 +02:00
Classic298
adda47ab04
move import 2025-08-12 22:06:10 +02:00
Classic298
2818b4643a
Update folders.py 2025-08-12 14:58:33 +02:00
Classic298
482030ff69
Update prune.py 2025-08-12 14:56:44 +02:00
Classic298
34c9a8825c
Update prune.py 2025-08-12 14:54:54 +02:00
Classic298
709c852917
Update prune.py 2025-08-12 13:20:16 +02:00
Classic298
60edac6c3f
Update Database.svelte 2025-08-12 13:16:55 +02:00
Classic298
e4a0bd8640
Update Database.svelte 2025-08-12 13:15:38 +02:00
Classic298
8d7273afae
Update prune.ts 2025-08-12 12:48:05 +02:00
Classic298
5ce002d5b3
Update PruneDataDialog.svelte 2025-08-12 12:47:51 +02:00
Classic298
0bd42e5c6d
Update Database.svelte 2025-08-12 12:47:34 +02:00
Classic298
028a2e5984
Update prune.py 2025-08-12 12:47:19 +02:00
Classic298
aadb296577
Merge branch 'open-webui:main' into universal_file_deletion 2025-08-11 22:09:25 +02:00
Classic298
d454e6a033
Feat/prune orphaned data (#16)
* feat: Add prune orphaned data functionality

* feat: Add prune orphaned data functionality

* feat: Add prune orphaned data functionality

* fix: Restyle PruneDataDialog modal

* feat: Add comprehensive prune orphaned data functionality and fix circular import

* feat: Add comprehensive prune orphaned data functionality and fix circular import

* feat: Add comprehensive prune orphaned data functionality and fix database size issues

* feat: Add comprehensive prune orphaned data functionality and fix database size issues

* feat: Add comprehensive prune orphaned data functionality and fix database size issues

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update folders.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Update prune.py

* Delete backend/open_webui/test/test_prune.py

* Update prune.ts

* Update PruneDataDialog.svelte

* Update prune.py

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update prune.py

* Update PruneDataDialog.svelte

* Update prune.ts

* Update Database.svelte

* Update prune.py

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update prune.py

* Update prune.py

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update Database.svelte

* Update prune.py

* Update prune.ts

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

* Update prune.py

* Update prune.ts

* Update PruneDataDialog.svelte

* Update files.py

* Update prompts.py

* Update notes.py

* Update models.py

* Update access_control.py

* Update PruneDataDialog.svelte

* Update PruneDataDialog.svelte

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-08-10 23:40:01 +02:00
7 changed files with 3016 additions and 1 deletions

View file

@ -88,6 +88,7 @@ from open_webui.routers import (
models,
knowledge,
prompts,
prune,
evaluations,
tools,
users,
@ -1401,6 +1402,7 @@ app.include_router(
evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
)
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
app.include_router(prune.router, prefix="/api/v1/prune", tags=["prune"])
# SCIM 2.0 API for identity management
if ENABLE_SCIM:

View file

@ -157,6 +157,12 @@ class FolderTable:
for folder in db.query(Folder).filter_by(user_id=user_id).all()
]
def get_all_folders(self) -> list[FolderModel]:
with get_db() as db:
return [
FolderModel.model_validate(folder) for folder in db.query(Folder).all()
]
def get_folder_by_parent_id_and_user_id_and_name(
self, parent_id: Optional[str], user_id: str, name: str
) -> Optional[FolderModel]:

File diff suppressed because it is too large Load diff

66
src/lib/apis/prune.ts Normal file
View file

@ -0,0 +1,66 @@
import { WEBUI_API_BASE_URL } from '$lib/constants';
export const pruneData = async (
token: string,
days: number | null,
exempt_archived_chats: boolean,
exempt_chats_in_folders: boolean,
delete_orphaned_chats: boolean = true,
delete_orphaned_tools: boolean = false,
delete_orphaned_functions: boolean = false,
delete_orphaned_prompts: boolean = true,
delete_orphaned_knowledge_bases: boolean = true,
delete_orphaned_models: boolean = true,
delete_orphaned_notes: boolean = true,
delete_orphaned_folders: boolean = true,
audio_cache_max_age_days: number | null = 30,
delete_inactive_users_days: number | null = null,
exempt_admin_users: boolean = true,
exempt_pending_users: boolean = true,
run_vacuum: boolean = false,
dry_run: boolean // Removed default value to ensure explicit passing
) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/prune/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
days,
exempt_archived_chats,
exempt_chats_in_folders,
delete_orphaned_chats,
delete_orphaned_tools,
delete_orphaned_functions,
delete_orphaned_prompts,
delete_orphaned_knowledge_bases,
delete_orphaned_models,
delete_orphaned_notes,
delete_orphaned_folders,
audio_cache_max_age_days,
delete_inactive_users_days,
exempt_admin_users,
exempt_pending_users,
run_vacuum,
dry_run
})
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.catch((err) => {
error = err;
console.log(err);
return null;
});
if (error) {
throw error;
}
return res;
};

View file

@ -10,10 +10,17 @@
import { getAllUsers } from '$lib/apis/users';
import { exportConfig, importConfig } from '$lib/apis/configs';
import PruneDataDialog from '$lib/components/common/PruneDataDialog.svelte';
import { pruneData } from '$lib/apis/prune';
const i18n = getContext('i18n');
export let saveHandler: Function;
let showPruneDataDialog = false;
let showPreviewResults = false;
let previewResults = null;
let lastPruneSettings = null;
const exportAllUserChats = async () => {
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], {
type: 'application/json'
@ -21,6 +28,77 @@
saveAs(blob, `all-chats-export-${Date.now()}.json`);
};
const handlePruneDataPreview = async (event) => {
const settings = event.detail;
lastPruneSettings = settings;
console.log('Preview call - dry_run should be TRUE');
const res = await pruneData(
localStorage.token,
settings.days,
settings.exempt_archived_chats,
settings.exempt_chats_in_folders,
settings.delete_orphaned_chats,
settings.delete_orphaned_tools,
settings.delete_orphaned_functions,
settings.delete_orphaned_prompts,
settings.delete_orphaned_knowledge_bases,
settings.delete_orphaned_models,
settings.delete_orphaned_notes,
settings.delete_orphaned_folders,
settings.audio_cache_max_age_days,
settings.delete_inactive_users_days,
settings.exempt_admin_users,
settings.exempt_pending_users,
settings.run_vacuum,
true // dry_run = true for preview
).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
previewResults = res;
showPreviewResults = true;
}
};
const handleConfirmPrune = async () => {
if (!lastPruneSettings) return;
console.log('Confirm call - dry_run should be FALSE');
const res = await pruneData(
localStorage.token,
lastPruneSettings.days,
lastPruneSettings.exempt_archived_chats,
lastPruneSettings.exempt_chats_in_folders,
lastPruneSettings.delete_orphaned_chats,
lastPruneSettings.delete_orphaned_tools,
lastPruneSettings.delete_orphaned_functions,
lastPruneSettings.delete_orphaned_prompts,
lastPruneSettings.delete_orphaned_knowledge_bases,
lastPruneSettings.delete_orphaned_models,
lastPruneSettings.delete_orphaned_notes,
lastPruneSettings.delete_orphaned_folders,
lastPruneSettings.audio_cache_max_age_days,
lastPruneSettings.delete_inactive_users_days,
lastPruneSettings.exempt_admin_users,
lastPruneSettings.exempt_pending_users,
lastPruneSettings.run_vacuum,
false // dry_run = false for actual pruning
).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res) {
toast.success('Data pruned successfully');
showPreviewResults = false;
previewResults = null;
lastPruneSettings = null;
}
};
const exportUsers = async () => {
const users = await getAllUsers(localStorage.token);
@ -49,6 +127,151 @@
});
</script>
<!-- Preview Results Modal -->
{#if showPreviewResults && previewResults}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[80vh] overflow-y-auto">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Pruning Preview Results')}
</h3>
<button
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
on:click={() => (showPreviewResults = false)}
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-4">
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">
{$i18n.t('The following items would be deleted:')}
</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
{#if previewResults.inactive_users > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Inactive users')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.inactive_users}</span>
</div>
{/if}
{#if previewResults.old_chats > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Old chats')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.old_chats}</span>
</div>
{/if}
{#if previewResults.orphaned_chats > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned chats')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_chats}</span>
</div>
{/if}
{#if previewResults.orphaned_files > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned files')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_files}</span>
</div>
{/if}
{#if previewResults.orphaned_tools > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned tools')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_tools}</span>
</div>
{/if}
{#if previewResults.orphaned_functions > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned functions')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_functions}</span>
</div>
{/if}
{#if previewResults.orphaned_prompts > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned prompts')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_prompts}</span>
</div>
{/if}
{#if previewResults.orphaned_knowledge_bases > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned knowledge bases')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_knowledge_bases}</span>
</div>
{/if}
{#if previewResults.orphaned_models > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned models')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_models}</span>
</div>
{/if}
{#if previewResults.orphaned_notes > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned notes')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_notes}</span>
</div>
{/if}
{#if previewResults.orphaned_folders > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned folders')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_folders}</span>
</div>
{/if}
{#if previewResults.orphaned_uploads > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned upload files')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_uploads}</span>
</div>
{/if}
{#if previewResults.orphaned_vector_collections > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Orphaned vector collections')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.orphaned_vector_collections}</span>
</div>
{/if}
{#if previewResults.audio_cache_files > 0}
<div class="flex justify-between">
<span class="text-gray-700 dark:text-gray-300">{$i18n.t('Audio cache files')}:</span>
<span class="font-medium text-red-600 dark:text-red-400">{previewResults.audio_cache_files}</span>
</div>
{/if}
</div>
{#if Object.values(previewResults).every(count => count === 0)}
<div class="text-center py-4">
<div class="text-green-600 dark:text-green-400 font-medium">
{$i18n.t('No items would be deleted with current settings')}
</div>
<div class="text-sm text-gray-500 dark:text-gray-400 mt-1">
{$i18n.t('Your system is already clean or no cleanup options are enabled')}
</div>
</div>
{/if}
</div>
<!-- Action buttons -->
<div class="flex justify-end gap-3 pt-4">
<button
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 transition-colors"
on:click={() => (showPreviewResults = false)}
>
{$i18n.t('Cancel')}
</button>
{#if !Object.values(previewResults).every(count => count === 0)}
<button
class="px-4 py-2 text-sm font-medium text-white bg-red-600 border border-transparent rounded-lg hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 transition-colors"
on:click={handleConfirmPrune}
>
{$i18n.t('Prune Data')}
</button>
{/if}
</div>
</div>
</div>
</div>
{/if}
<PruneDataDialog bind:show={showPruneDataDialog} on:preview={handlePruneDataPreview} />
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
@ -231,6 +454,32 @@
</div>
</button>
{/if}
<hr class="border-gray-100 dark:border-gray-850 my-1" />
<button
type="button"
class=" flex rounded-md py-2 px-3 w-full bg-yellow-500 hover:bg-yellow-600 text-white transition"
on:click={() => {
showPruneDataDialog = true;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M4.5 2a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-7ZM3 6a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1 4a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1H4.5a.5.5 0 0 1-.5-.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center text-sm font-medium">
{$i18n.t('Prune Orphaned Data')}
</div>
</button>
</div>
</div>
</form>

View file

@ -339,7 +339,7 @@
</tr>
</thead>
<tbody class="">
{#each users as user, userIdx}
{#each users as user, userIdx (user.id)}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<td class="px-3 py-1 min-w-[7rem] w-28">
<button

View file

@ -0,0 +1,899 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
import Switch from '$lib/components/common/Switch.svelte';
const i18n = getContext('i18n');
export let show = false;
let deleteChatsByAge = false;
let days = 60;
let exempt_archived_chats = true;
let exempt_chats_in_folders = false;
// Inactive user deletion
let deleteInactiveUsers = false;
let delete_inactive_users_days = 90;
let exempt_admin_users = true;
let exempt_pending_users = true;
// Orphaned resource deletion toggles
let delete_orphaned_chats = true;
let delete_orphaned_tools = false;
let delete_orphaned_functions = false;
let delete_orphaned_prompts = true;
let delete_orphaned_knowledge_bases = true;
let delete_orphaned_models = true;
let delete_orphaned_notes = true;
let delete_orphaned_folders = true;
// Audio cache cleanup
let cleanupAudioCache = true;
let audio_cache_max_age_days = 30;
// System/Database optimization
let run_vacuum = false;
let showDetailsExpanded = false;
let activeDetailsTab = 'users';
let activeSettingsTab = 'users';
let showApiPreview = false;
const dispatch = createEventDispatcher();
const preview = () => {
dispatch('preview', {
days: deleteChatsByAge ? days : null,
exempt_archived_chats,
exempt_chats_in_folders,
delete_orphaned_chats,
delete_orphaned_tools,
delete_orphaned_functions,
delete_orphaned_prompts,
delete_orphaned_knowledge_bases,
delete_orphaned_models,
delete_orphaned_notes,
delete_orphaned_folders,
audio_cache_max_age_days: cleanupAudioCache ? audio_cache_max_age_days : null,
delete_inactive_users_days: deleteInactiveUsers ? delete_inactive_users_days : null,
exempt_admin_users,
exempt_pending_users,
run_vacuum
});
show = false;
};
// Generate API call preview with helpful comments
$: apiCallPreview = `# Open WebUI Data Pruning API Call
# Use this template for automated maintenance scripts (cron jobs, etc.)
# AUTHENTICATION: Use API Key (not JWT token) for automation
# Get your API key from: Settings → Account → API Key → Generate new key
# Format: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
curl -X POST "${window.location.origin}/api/v1/prune/" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer <your-api-key>" \\
-d '{
// SAFETY: Always test with dry_run=true first to preview results
"dry_run": false,
// AGE-BASED CHAT DELETION (null = disabled)
"days": ${deleteChatsByAge ? days : null},
"exempt_archived_chats": ${exempt_archived_chats}, // Keep archived chats even if old
"exempt_chats_in_folders": ${exempt_chats_in_folders}, // Keep organized/pinned chats
// INACTIVE USER DELETION (null = disabled, VERY DESTRUCTIVE)
"delete_inactive_users_days": ${deleteInactiveUsers ? delete_inactive_users_days : null},
"exempt_admin_users": ${exempt_admin_users}, // Strongly recommended: true
"exempt_pending_users": ${exempt_pending_users}, // Recommended for user approval workflows
// ORPHANED DATA CLEANUP (from deleted users)
"delete_orphaned_chats": ${delete_orphaned_chats},
"delete_orphaned_tools": ${delete_orphaned_tools},
"delete_orphaned_functions": ${delete_orphaned_functions}, // Actions, Pipes, Filters
"delete_orphaned_prompts": ${delete_orphaned_prompts},
"delete_orphaned_knowledge_bases": ${delete_orphaned_knowledge_bases},
"delete_orphaned_models": ${delete_orphaned_models},
"delete_orphaned_notes": ${delete_orphaned_notes},
"delete_orphaned_folders": ${delete_orphaned_folders},
// AUDIO CACHE CLEANUP (null = disabled)
"audio_cache_max_age_days": ${cleanupAudioCache ? audio_cache_max_age_days : null}, // TTS/STT files
// DATABASE OPTIMIZATION (WARNING: Locks database during execution!)
"run_vacuum": ${run_vacuum} // Reclaim disk space - only enable during maintenance windows
}'
# API KEY vs JWT TOKEN:
# - API Key: Persistent, use for automation (sk-xxxxxxxx...)
# - JWT Token: Session-bound, temporary, use for web UI only
# - ALWAYS use API Key for scripts/cron jobs
# AUTOMATION TIPS:
# 1. Run with dry_run=true first to preview what will be deleted
# 2. Schedule during low-usage hours to minimize performance impact
# 3. Monitor logs: tail -f /path/to/open-webui/logs
# 4. Consider database backup before large cleanup operations
# 5. Test on staging environment with similar data size first
# EXAMPLE CRON JOB (runs weekly on Sunday at 2 AM):
# 0 2 * * 0 /path/to/your/prune-script.sh >> /var/log/openwebui-prune.log 2>&1
# RESPONSE HANDLING:
# - dry_run=true: Returns counts object with preview numbers
# - dry_run=false: Returns true on success, throws error on failure
# - Always check HTTP status code and response for errors`;
// Warning for short inactive user deletion periods
$: shortUserDeletionWarning = deleteInactiveUsers && delete_inactive_users_days < 30;
const copyApiCall = () => {
navigator.clipboard.writeText(apiCallPreview).then(() => {
// Could add a toast notification here
console.log('API call copied to clipboard');
}).catch(err => {
console.error('Failed to copy API call: ', err);
});
};
</script>
<Modal bind:show size="lg">
<div>
<div class="flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class="text-lg font-medium self-center">
{$i18n.t('Prune Orphaned Data')}
</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col w-full px-5 pb-5 dark:text-gray-200">
<div class="space-y-4">
<!-- Critical Warning Message -->
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200 mb-2">
{$i18n.t('Destructive Operation - Backup Recommended')}
</h3>
<div class="text-sm text-red-700 dark:text-red-300 space-y-1">
<p>{$i18n.t('This action will permanently delete data from your database. Only orphaned or old data, based on your configuration settings, will be deleted. All active, referenced data remains completely safe.')}</p>
<p>{$i18n.t('This operation cannot be undone. Create a complete backup of your database and files before proceeding. This operation is performed entirely at your own risk - having a backup ensures you can restore any data if something unexpected occurs.')}</p>
<!-- Expandable Details Section -->
<div class="mt-3">
<button
class="flex items-center text-xs text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200 focus:outline-none"
on:click={() => showDetailsExpanded = !showDetailsExpanded}
>
<svg
class="w-3 h-3 mr-1 transition-transform duration-200 {showDetailsExpanded ? 'rotate-90' : ''}"
fill="currentColor"
viewBox="0 0 20 20"
>
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
{showDetailsExpanded ? $i18n.t('Hide details') : $i18n.t('Show details')}
</button>
{#if showDetailsExpanded}
<div class="mt-2 pl-4 border-l-2 border-red-300 dark:border-red-700 text-xs text-red-600 dark:text-red-400">
<p class="mb-3"><strong>{$i18n.t('Note:')}</strong> {$i18n.t('This list provides an overview of what will be deleted during the pruning process and may not be complete or fully up-to-date.')}</p>
<!-- Tab Navigation -->
<div class="flex flex-wrap gap-1 mb-3 border-b border-red-300 dark:border-red-700">
<button
class="px-2 py-1 text-xs font-medium rounded-t transition-colors {activeDetailsTab === 'users' ? 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200'}"
on:click={() => activeDetailsTab = 'users'}
>
{$i18n.t('Users')}
</button>
<button
class="px-2 py-1 text-xs font-medium rounded-t transition-colors {activeDetailsTab === 'chats' ? 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200'}"
on:click={() => activeDetailsTab = 'chats'}
>
{$i18n.t('Chats')}
</button>
<button
class="px-2 py-1 text-xs font-medium rounded-t transition-colors {activeDetailsTab === 'workspace' ? 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200'}"
on:click={() => activeDetailsTab = 'workspace'}
>
{$i18n.t('Workspace')}
</button>
<button
class="px-2 py-1 text-xs font-medium rounded-t transition-colors {activeDetailsTab === 'datavector' ? 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200'}"
on:click={() => activeDetailsTab = 'datavector'}
>
{$i18n.t('Data & Vector')}
</button>
<button
class="px-2 py-1 text-xs font-medium rounded-t transition-colors {activeDetailsTab === 'imagesaudio' ? 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200'}"
on:click={() => activeDetailsTab = 'imagesaudio'}
>
{$i18n.t('Images & Audio')}
</button>
<button
class="px-2 py-1 text-xs font-medium rounded-t transition-colors {activeDetailsTab === 'system' ? 'bg-red-100 dark:bg-red-800 text-red-800 dark:text-red-200' : 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200'}"
on:click={() => activeDetailsTab = 'system'}
>
{$i18n.t('System & Database')}
</button>
</div>
<!-- Tab Content -->
<div class="space-y-2">
{#if activeDetailsTab === 'users'}
<div class="space-y-1">
<p><strong>{$i18n.t('Inactive User Account Deletion:')}</strong></p>
<p>{$i18n.t('Removes user accounts that have been inactive for a specified period based on their last activity timestamp')}</p>
<p class="pt-2"><strong>{$i18n.t('Safety Exemptions:')}</strong></p>
<p>{$i18n.t('Admin users: Can be exempted from deletion (recommended)')}</p>
<p>{$i18n.t('Pending users: Can be exempted from deletion')}</p>
</div>
{:else if activeDetailsTab === 'chats'}
<div class="space-y-1">
<p><strong>{$i18n.t('Age-Based Chat Deletion:')}</strong></p>
<p>{$i18n.t('Removes conversations older than specified days based on when they were last modified or updated (not when they were created)')}</p>
<p>{$i18n.t('Supports exemptions for:')}</p>
<p class="ml-4">{$i18n.t('Archived chats')}</p>
<p class="ml-4">{$i18n.t('Chats organized in folders and pinned chats')}</p>
<p class="pt-2"><strong>{$i18n.t('Orphaned Content Cleanup:')}</strong></p>
<p>{$i18n.t('Delete orphaned chats from deleted users')}</p>
<p>{$i18n.t('Delete orphaned folders from deleted users')}</p>
</div>
{:else if activeDetailsTab === 'workspace'}
<div class="space-y-1">
<p><strong>{$i18n.t('Orphaned Workspace Items from Deleted Users:')}</strong></p>
<p>{$i18n.t('Delete orphaned knowledge bases')}</p>
<p>{$i18n.t('Delete orphaned custom tools')}</p>
<p>{$i18n.t('Delete orphaned custom functions (Actions, Pipes, Filters)')}</p>
<p>{$i18n.t('Delete orphaned custom prompts and templates')}</p>
<p>{$i18n.t('Delete orphaned custom models and configurations')}</p>
<p>{$i18n.t('Delete orphaned notes')}</p>
</div>
{:else if activeDetailsTab === 'datavector'}
<div class="space-y-1">
<p><strong>{$i18n.t('Files & Vector Storage:')}</strong></p>
<p>{$i18n.t('Orphaned files and attachments from deleted content')}</p>
<p>{$i18n.t('Vector embeddings and collections for removed data')}</p>
<p>{$i18n.t('Uploaded files that lost their database references')}</p>
<p>{$i18n.t('Vector storage directories without corresponding data')}</p>
</div>
{:else if activeDetailsTab === 'imagesaudio'}
<div class="space-y-1">
<p><strong>{$i18n.t('Images & Audio Content Cleanup:')}</strong></p>
<p>{$i18n.t('Generated images: Already integrated with file system - orphaned images are automatically cleaned up when chats are deleted')}</p>
<p>{$i18n.t('Uploaded images: Already integrated with file system - orphaned images are automatically cleaned up based on active references')}</p>
<p>{$i18n.t('Audio cache cleanup: Remove old text-to-speech (TTS) generated audio files and speech-to-text (STT) transcription files')}</p>
<p>{$i18n.t('Audio recordings and transcriptions: Clean up cached audio files older than specified days')}</p>
</div>
{:else if activeDetailsTab === 'system'}
<div class="space-y-1">
<p><strong>{$i18n.t('Database & System Cleanup:')}</strong></p>
<p>{$i18n.t('Removal of broken database references and stale entries')}</p>
<p>{$i18n.t('Disk space reclamation by database cleanup')}</p>
<p>{$i18n.t('Synchronization of database records with actual file storage')}</p>
<p>{$i18n.t('Fix inconsistencies between storage systems')}</p>
<p>{$i18n.t('Database performance optimization')}</p>
</div>
{/if}
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
</div>
<!-- Performance Warning -->
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-yellow-800 dark:text-yellow-200">
{$i18n.t('Performance Warning: This operation may take a very long time to complete, especially if you have never cleaned your database before or if your instance stores large amounts of data. The process could take anywhere from seconds, to minutes, to half an hour and beyond depending on your data size.')}
</p>
</div>
</div>
</div>
<!-- Settings Section with Tabs -->
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<div class="flex items-center mb-3">
<svg class="h-4 w-4 text-blue-600 dark:text-blue-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" />
</svg>
<h4 class="text-sm font-medium text-blue-800 dark:text-blue-200">
{$i18n.t('Pruning Configuration')}
</h4>
</div>
<p class="text-xs text-blue-700 dark:text-blue-300 mb-4">
{$i18n.t('Configure what data should be cleaned up during the pruning process.')}
</p>
<!-- Settings Tab Navigation -->
<div class="flex flex-wrap gap-1 mb-4 border-b border-blue-300 dark:border-blue-700">
<button
class="px-3 py-2 text-sm font-medium rounded-t transition-colors {activeSettingsTab === 'users' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200'}"
on:click={() => activeSettingsTab = 'users'}
>
{$i18n.t('Users')}
</button>
<button
class="px-3 py-2 text-sm font-medium rounded-t transition-colors {activeSettingsTab === 'chats' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200'}"
on:click={() => activeSettingsTab = 'chats'}
>
{$i18n.t('Chats')}
</button>
<button
class="px-3 py-2 text-sm font-medium rounded-t transition-colors {activeSettingsTab === 'workspace' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200'}"
on:click={() => activeSettingsTab = 'workspace'}
>
{$i18n.t('Workspace')}
</button>
<button
class="px-3 py-2 text-sm font-medium rounded-t transition-colors {activeSettingsTab === 'audio' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200'}"
on:click={() => activeSettingsTab = 'audio'}
>
{$i18n.t('Audio Cache')}
</button>
<button
class="px-3 py-2 text-sm font-medium rounded-t transition-colors {activeSettingsTab === 'system' ? 'bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200' : 'text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-200'}"
on:click={() => activeSettingsTab = 'system'}
>
{$i18n.t('System')}
</button>
</div>
<!-- Settings Tab Content -->
<div class="space-y-4">
{#if activeSettingsTab === 'users'}
<!-- Inactive User Deletion -->
<div class="space-y-4">
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={deleteInactiveUsers} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete inactive user accounts')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Remove user accounts inactive for specified days')}
</div>
</div>
</div>
</div>
<!-- User Deletion Options (when enabled) -->
{#if deleteInactiveUsers}
<div class="ml-8 space-y-4 border-l-2 border-red-200 dark:border-red-700 pl-4">
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{$i18n.t('Delete users inactive for more than')}
</label>
<div class="flex items-center space-x-2">
<input
id="user-days"
type="number"
min="1"
bind:value={delete_inactive_users_days}
class="w-20 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{$i18n.t('days')}</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Based on last_active_at timestamp. Minimum 1 day.')}
</p>
<!-- Warning for short periods -->
{#if shortUserDeletionWarning}
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-4 w-4 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-2">
<p class="text-xs text-red-800 dark:text-red-200 font-medium">
{$i18n.t('⚠️ Warning: Deletion period less than 30 days!')}
</p>
<p class="text-xs text-red-700 dark:text-red-300 mt-1">
{$i18n.t('Very short periods may accidentally delete active users. Consider using 30+ days for safety.')}
</p>
</div>
</div>
</div>
{/if}
</div>
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={exempt_admin_users} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Exempt admin users')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Never delete admin users (strongly recommended)')}
</div>
</div>
</div>
</div>
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={exempt_pending_users} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Exempt pending users')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Never delete pending/unapproved users')}
</div>
</div>
</div>
</div>
</div>
{/if}
</div>
{:else if activeSettingsTab === 'chats'}
<!-- Age-Based Chat Deletion -->
<div class="space-y-4">
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={deleteChatsByAge} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete chats by age')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Optionally remove old chats based on last update time')}
</div>
</div>
</div>
</div>
<!-- Chat Options (when enabled) -->
{#if deleteChatsByAge}
<div class="ml-8 space-y-4 border-l-2 border-gray-200 dark:border-gray-700 pl-4">
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{$i18n.t('Delete chats older than')}
</label>
<div class="flex items-center space-x-2">
<input
id="days"
type="number"
min="0"
bind:value={days}
class="w-20 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{$i18n.t('days')}</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Set to 0 to delete all chats, or specify number of days')}
</p>
</div>
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={exempt_archived_chats} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Exempt archived chats')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Keep archived chats even if they are old')}
</div>
</div>
</div>
</div>
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={exempt_chats_in_folders} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Exempt chats in folders')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Keep chats that are organized in folders or pinned')}
</div>
</div>
</div>
</div>
</div>
{/if}
<!-- Orphaned Chat Deletion -->
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_chats} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned chats')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned chats from deleted users')}
</div>
</div>
</div>
</div>
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_folders} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned folders')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned folders from deleted users')}
</div>
</div>
</div>
</div>
</div>
</div>
{:else if activeSettingsTab === 'workspace'}
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Knowledge Bases -->
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_knowledge_bases} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned knowledge bases')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned knowledge bases from deleted users')}
</div>
</div>
</div>
</div>
<!-- Tools -->
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_tools} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned tools')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned custom tools from deleted users')}
</div>
</div>
</div>
</div>
<!-- Functions -->
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_functions} />
</div>
<div>
<div class="flex items-center text-sm font-medium text-gray-900 dark:text-gray-100">
<span>{$i18n.t('Delete orphaned functions')}</span>
<div class="relative group ml-2">
<svg class="h-3 w-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-help" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
<div class="absolute left-1/2 transform -translate-x-1/2 bottom-full mb-2 w-48 px-3 py-2 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10">
<div class="font-medium mb-1">{$i18n.t('Admin panel functions - all functions, including:')}</div>
<div class="space-y-0.5">
<div>{$i18n.t('Actions')}</div>
<div>{$i18n.t('Pipes')}</div>
<div>{$i18n.t('Filters')}</div>
</div>
<div class="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
</div>
</div>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned custom functions from deleted users')}
</div>
</div>
</div>
</div>
<!-- Prompts -->
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_prompts} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned prompts')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned custom prompts from deleted users')}
</div>
</div>
</div>
</div>
<!-- Models -->
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_models} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned models')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned custom models from deleted users')}
</div>
</div>
</div>
</div>
<!-- Notes -->
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={delete_orphaned_notes} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Delete orphaned notes')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Delete orphaned notes from deleted users')}
</div>
</div>
</div>
</div>
</div>
{:else if activeSettingsTab === 'audio'}
<!-- Audio Cache Cleanup -->
<div class="space-y-4">
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={cleanupAudioCache} />
</div>
<div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{$i18n.t('Clean audio cache')}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Remove old audio cache files (TTS and STT recordings)')}
</div>
</div>
</div>
</div>
<!-- Audio Cache Options (when enabled) -->
{#if cleanupAudioCache}
<div class="ml-8 space-y-4 border-l-2 border-gray-200 dark:border-gray-700 pl-4">
<div class="space-y-2">
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
{$i18n.t('Delete audio files older than')}
</label>
<div class="flex items-center space-x-2">
<input
id="audio-days"
type="number"
min="0"
bind:value={audio_cache_max_age_days}
class="w-20 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
<span class="text-sm text-gray-700 dark:text-gray-300">{$i18n.t('days')}</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Remove cached TTS (text-to-speech) and STT (speech-to-text) files older than specified days')}
</p>
</div>
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<h5 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-2">
{$i18n.t('Audio Cache Types:')}
</h5>
<div class="space-y-1 text-xs text-gray-600 dark:text-gray-400">
<p><strong>{$i18n.t('TTS Files:')}</strong> {$i18n.t('Generated audio files when AI speaks text to you')}</p>
<p><strong>{$i18n.t('STT Files:')}</strong> {$i18n.t('Uploaded audio files for transcription (voice messages)')}</p>
<p><strong>{$i18n.t('Metadata:')}</strong> {$i18n.t('Associated JSON files with transcription data')}</p>
</div>
</div>
</div>
{/if}
</div>
{:else if activeSettingsTab === 'system'}
<!-- System/Database Optimization -->
<div class="space-y-4">
<div class="flex items-start py-2">
<div class="flex items-center">
<div class="mr-3">
<Switch bind:state={run_vacuum} />
</div>
<div class="flex-1">
<div class="flex items-center text-sm font-medium text-gray-900 dark:text-gray-100">
<span>{$i18n.t('Run VACUUM optimization')}</span>
<div class="relative group ml-2">
<svg class="h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-help" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
<div class="absolute left-1/2 transform -translate-x-1/2 bottom-full mb-2 w-72 px-3 py-2 text-xs text-white bg-gray-900 dark:bg-gray-700 rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-10">
<div class="font-medium mb-1">{$i18n.t('Database Optimization Warning:')}</div>
<div class="space-y-1">
<p>{$i18n.t('VACUUM reclaims disk space by rebuilding the database file.')}</p>
<p class="text-yellow-300 dark:text-yellow-400 font-medium">{$i18n.t('⚠️ This may take a very long time on large databases and will LOCK the entire database during execution.')}</p>
<p>{$i18n.t('It is strongly recommended to NOT run this while users are actively using the platform.')}</p>
<p class="text-green-300 dark:text-green-400">{$i18n.t('💡 Best practice: Run during scheduled maintenance windows.')}</p>
</div>
<div class="absolute top-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-t-gray-900 dark:border-t-gray-700"></div>
</div>
</div>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{$i18n.t('Reclaim disk space after cleanup (locks database during operation)')}
</div>
</div>
</div>
</div>
<!-- VACUUM warning box -->
{#if run_vacuum}
<div class="ml-8 border-l-2 border-yellow-200 dark:border-yellow-700 pl-4">
<div class="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h4 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
{$i18n.t('VACUUM Enabled - Important Considerations:')}
</h4>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300 space-y-1">
<p>{$i18n.t('Database will be locked during VACUUM - all users will experience errors')}</p>
<p>{$i18n.t('Operation duration depends on database size (can be 5-30+ minutes)')}</p>
<p>{$i18n.t('Recommended only during scheduled maintenance windows')}</p>
<p>{$i18n.t('Not required for routine cleanups - only when reclaiming disk space is critical')}</p>
</div>
</div>
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
</div>
<!-- API Call Preview Section -->
<div class="bg-gray-50 dark:bg-gray-900/20 border border-gray-200 dark:border-gray-800 rounded-lg p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2">
{$i18n.t('API Automation Helper')}
</h3>
<button
class="flex items-center text-xs text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 focus:outline-none mb-3"
on:click={() => showApiPreview = !showApiPreview}
>
<svg
class="w-3 h-3 mr-1 transition-transform duration-200 {showApiPreview ? 'rotate-90' : ''}"
fill="currentColor"
viewBox="0 0 20 20"
>
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
{showApiPreview ? $i18n.t('Hide API call') : $i18n.t('Show API call')}
</button>
{#if showApiPreview}
<div class="space-y-2">
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">
{$i18n.t('Use this API call configuration to automate pruning operations in your own maintenance scripts.')}
</p>
<div class="relative">
<textarea
readonly
value={apiCallPreview}
class="w-full h-40 px-3 py-2 text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-gray-100 resize-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500"
on:focus={(e) => e.target.select()}
></textarea>
<button
class="absolute top-2 right-2 px-2 py-1 text-xs font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded focus:outline-none focus:ring-2 focus:ring-gray-500"
on:click={copyApiCall}
title={$i18n.t('Copy to clipboard')}
>
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z"></path>
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z"></path>
</svg>
</button>
</div>
</div>
{/if}
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-6 flex justify-end gap-3">
<button
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 transition-colors"
on:click={() => (show = false)}
>
{$i18n.t('Cancel')}
</button>
<button
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors"
on:click={preview}
>
{$i18n.t('Preview')}
</button>
</div>
</div>
</div>
</Modal>