mirror of
https://github.com/open-webui/open-webui.git
synced 2025-12-12 04:15:25 +00:00
Merge 81c7617508 into 3b3e12b43a
This commit is contained in:
commit
097f33aec3
6 changed files with 3015 additions and 0 deletions
|
|
@ -88,6 +88,7 @@ from open_webui.routers import (
|
||||||
models,
|
models,
|
||||||
knowledge,
|
knowledge,
|
||||||
prompts,
|
prompts,
|
||||||
|
prune,
|
||||||
evaluations,
|
evaluations,
|
||||||
tools,
|
tools,
|
||||||
users,
|
users,
|
||||||
|
|
@ -1401,6 +1402,7 @@ app.include_router(
|
||||||
evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
|
evaluations.router, prefix="/api/v1/evaluations", tags=["evaluations"]
|
||||||
)
|
)
|
||||||
app.include_router(utils.router, prefix="/api/v1/utils", tags=["utils"])
|
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
|
# SCIM 2.0 API for identity management
|
||||||
if ENABLE_SCIM:
|
if ENABLE_SCIM:
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,12 @@ class FolderTable:
|
||||||
for folder in db.query(Folder).filter_by(user_id=user_id).all()
|
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(
|
def get_folder_by_parent_id_and_user_id_and_name(
|
||||||
self, parent_id: Optional[str], user_id: str, name: str
|
self, parent_id: Optional[str], user_id: str, name: str
|
||||||
) -> Optional[FolderModel]:
|
) -> Optional[FolderModel]:
|
||||||
|
|
|
||||||
1793
backend/open_webui/routers/prune.py
Normal file
1793
backend/open_webui/routers/prune.py
Normal file
File diff suppressed because it is too large
Load diff
66
src/lib/apis/prune.ts
Normal file
66
src/lib/apis/prune.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -10,16 +10,94 @@
|
||||||
import { getAllUsers } from '$lib/apis/users';
|
import { getAllUsers } from '$lib/apis/users';
|
||||||
import { exportConfig, importConfig } from '$lib/apis/configs';
|
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');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let saveHandler: Function;
|
export let saveHandler: Function;
|
||||||
|
|
||||||
|
let showPruneDataDialog = false;
|
||||||
|
let showPreviewResults = false;
|
||||||
|
let previewResults = null;
|
||||||
|
let lastPruneSettings = null;
|
||||||
|
|
||||||
const exportAllUserChats = async () => {
|
const exportAllUserChats = async () => {
|
||||||
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], {
|
let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], {
|
||||||
type: 'application/json'
|
type: 'application/json'
|
||||||
});
|
});
|
||||||
saveAs(blob, `all-chats-export-${Date.now()}.json`);
|
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 exportUsers = async () => {
|
||||||
const users = await getAllUsers(localStorage.token);
|
const users = await getAllUsers(localStorage.token);
|
||||||
|
|
@ -49,6 +127,151 @@
|
||||||
});
|
});
|
||||||
</script>
|
</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
|
<form
|
||||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||||
on:submit|preventDefault={async () => {
|
on:submit|preventDefault={async () => {
|
||||||
|
|
@ -231,6 +454,32 @@
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
899
src/lib/components/common/PruneDataDialog.svelte
Normal file
899
src/lib/components/common/PruneDataDialog.svelte
Normal 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>
|
||||||
Loading…
Reference in a new issue