feat(usage): add confirmed database cleanup controls
This commit is contained in:
1 parent
93525f0ac7
commit
bd57545c7f
7 files changed
+181
-15
No files matched your search
+10
-1
@@ -448,7 +448,6 @@ export const jaOverrides = {
|
||||
'usage.pricing.deleted': 'モデル料金を削除し、概要のコストを更新しました',
|
||||
'usage.pricing.deleteConfirm': 'モデル {model} の料金を削除しますか?',
|
||||
'usage.dataManagement.title': '履歴データ管理',
|
||||
'usage.dataManagement.description': '使用履歴の保存容量を管理し、既知の履歴データ異常を修復します。',
|
||||
'usage.dataManagement.manualBadge': '手動操作',
|
||||
'usage.dataManagement.storageTitle': 'データベース容量上限',
|
||||
'usage.dataManagement.storageDescription': '0 にすると無制限です。上限に達すると古い使用履歴から自動的に削除します。',
|
||||
@@ -460,6 +459,16 @@ export const jaOverrides = {
|
||||
'usage.dataManagement.storageInvalid': 'データベース容量上限には 0 または正の整数を入力してください。',
|
||||
'usage.dataManagement.storageSaved': 'データベース容量上限を保存しました。',
|
||||
'usage.dataManagement.storageSavedWithCleanup': 'データベース容量上限を保存し、古い記録を {deleted} 件削除しました。',
|
||||
'usage.dataManagement.shrinkTitle': 'データベースをワンクリックで縮小',
|
||||
'usage.dataManagement.shrinkDescription': 'データベースを目標サイズに調整します。確認後、目標サイズ以下になるまで古い使用履歴から削除します。',
|
||||
'usage.dataManagement.shrinkInput': '縮小後の目標サイズ(MB)',
|
||||
'usage.dataManagement.shrinkRun': '今すぐ縮小',
|
||||
'usage.dataManagement.shrinking': '縮小中…',
|
||||
'usage.dataManagement.shrinkInvalid': '縮小後の目標サイズには 0 より大きい整数を入力してください。',
|
||||
'usage.dataManagement.shrinkConfirmTitle': 'データベース縮小の確認',
|
||||
'usage.dataManagement.shrinkConfirm': 'データベースを {size} MB 以下に縮小しますか?古い使用履歴から削除され、この操作は元に戻せません。',
|
||||
'usage.dataManagement.shrinkSuccess': '縮小が完了しました。古い記録を {deleted} 件削除し、現在の使用量は {size} です。',
|
||||
'usage.dataManagement.shrinkNoCleanup': 'データベースを整理しました。現在の使用量は {size} のため、使用履歴は削除されませんでした。',
|
||||
'usage.dataManagement.actionTitle': 'キャッシュ記録を修復',
|
||||
'usage.dataManagement.actionDescription': 'キャッシュ読み取りと作成を含む入力 Token の集計方式に統一します。',
|
||||
'usage.dataManagement.run': 'スキャンして修復',
|
||||
|
||||
+10
-1
@@ -435,7 +435,6 @@ export const en: Record<MessageKey, string> = {
|
||||
'usage.pricing.deleted': 'Model price deleted; overview cost updated',
|
||||
'usage.pricing.deleteConfirm': 'Delete the price for {model}?',
|
||||
'usage.dataManagement.title': 'Historical Data Management',
|
||||
'usage.dataManagement.description': 'Manage usage record storage and repair known historical data anomalies.',
|
||||
'usage.dataManagement.manualBadge': 'Manual action',
|
||||
'usage.dataManagement.storageTitle': 'Database size limit',
|
||||
'usage.dataManagement.storageDescription': 'Set to 0 for no limit. When the limit is reached, the oldest usage records are deleted automatically.',
|
||||
@@ -447,6 +446,16 @@ export const en: Record<MessageKey, string> = {
|
||||
'usage.dataManagement.storageInvalid': 'Enter 0 or a positive integer for the database size limit.',
|
||||
'usage.dataManagement.storageSaved': 'Database size limit saved.',
|
||||
'usage.dataManagement.storageSavedWithCleanup': 'Database size limit saved and {deleted} oldest records deleted.',
|
||||
'usage.dataManagement.shrinkTitle': 'One-click database cleanup',
|
||||
'usage.dataManagement.shrinkDescription': 'Set the database to the target size. After confirmation, the oldest usage records are deleted until the database is at or below that size.',
|
||||
'usage.dataManagement.shrinkInput': 'Cleanup target size (MB)',
|
||||
'usage.dataManagement.shrinkRun': 'Clean up now',
|
||||
'usage.dataManagement.shrinking': 'Cleaning up…',
|
||||
'usage.dataManagement.shrinkInvalid': 'Enter a positive integer for the cleanup target size.',
|
||||
'usage.dataManagement.shrinkConfirmTitle': 'Confirm database cleanup',
|
||||
'usage.dataManagement.shrinkConfirm': 'Reduce the database to {size} MB or less? The oldest usage records will be deleted and this action cannot be undone.',
|
||||
'usage.dataManagement.shrinkSuccess': 'Cleanup complete. Deleted {deleted} oldest records; the database now uses {size}.',
|
||||
'usage.dataManagement.shrinkNoCleanup': 'Database cleanup complete. It currently uses {size}, so no usage records were deleted.',
|
||||
'usage.dataManagement.actionTitle': 'Repair historical anomalies',
|
||||
'usage.dataManagement.actionDescription': 'Correct Claude cache token accounting and delete records recorded as unknown usage.',
|
||||
'usage.dataManagement.run': 'Scan and repair',
|
||||
|
||||
@@ -434,7 +434,6 @@ export const zhCN = {
|
||||
'usage.pricing.deleted': '模型价格已删除,总览成本已更新',
|
||||
'usage.pricing.deleteConfirm': '确定删除模型 {model} 的价格吗?',
|
||||
'usage.dataManagement.title': '历史数据管理',
|
||||
'usage.dataManagement.description': '管理使用记录存储并修复历史数据中的已知异常。',
|
||||
'usage.dataManagement.manualBadge': '手动操作',
|
||||
'usage.dataManagement.storageTitle': '数据库大小限制',
|
||||
'usage.dataManagement.storageDescription': '设为 0 时不限制;达到上限后自动删除最旧的使用记录。',
|
||||
@@ -446,6 +445,16 @@ export const zhCN = {
|
||||
'usage.dataManagement.storageInvalid': '请输入 0 或正整数作为数据库大小上限。',
|
||||
'usage.dataManagement.storageSaved': '数据库大小限制已保存。',
|
||||
'usage.dataManagement.storageSavedWithCleanup': '数据库大小限制已保存,并删除了最旧的 {deleted} 条记录。',
|
||||
'usage.dataManagement.shrinkTitle': '数据库一键瘦身',
|
||||
'usage.dataManagement.shrinkDescription': '将数据库大小调整到目标大小,确认后将从最旧的使用记录开始删除,直到数据库小于或等于目标大小。',
|
||||
'usage.dataManagement.shrinkInput': '瘦身目标大小(MB)',
|
||||
'usage.dataManagement.shrinkRun': '一键瘦身',
|
||||
'usage.dataManagement.shrinking': '瘦身中…',
|
||||
'usage.dataManagement.shrinkInvalid': '请输入大于 0 的整数作为瘦身目标大小。',
|
||||
'usage.dataManagement.shrinkConfirmTitle': '确认数据库瘦身',
|
||||
'usage.dataManagement.shrinkConfirm': '确定将数据库瘦身到 {size} MB 以内吗?确认后将从最旧的使用记录开始删除,此操作无法撤销。',
|
||||
'usage.dataManagement.shrinkSuccess': '瘦身完成,已删除最旧的 {deleted} 条记录,当前占用 {size}。',
|
||||
'usage.dataManagement.shrinkNoCleanup': '数据库已整理,当前占用 {size},无需删除使用记录。',
|
||||
'usage.dataManagement.actionTitle': '修复历史异常记录',
|
||||
'usage.dataManagement.actionDescription': '修正错误缓存信息,并删除被误记为 unknown 的记录。',
|
||||
'usage.dataManagement.run': '扫描并修复',
|
||||
|
||||
@@ -733,6 +733,8 @@ function UsageDataManagementView() {
|
||||
const [limitDraft, setLimitDraft] = useState('0');
|
||||
const [loadingLimit, setLoadingLimit] = useState(true);
|
||||
const [savingLimit, setSavingLimit] = useState(false);
|
||||
const [shrinkDraft, setShrinkDraft] = useState('');
|
||||
const [shrinking, setShrinking] = useState(false);
|
||||
const [storageNotice, setStorageNotice] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -781,6 +783,40 @@ function UsageDataManagementView() {
|
||||
}
|
||||
};
|
||||
|
||||
const shrinkDatabase = async () => {
|
||||
const normalized = shrinkDraft.trim();
|
||||
const targetDatabaseSizeMb = Number(normalized);
|
||||
if (!/^\d+$/.test(normalized) || !Number.isSafeInteger(targetDatabaseSizeMb) || targetDatabaseSizeMb <= 0) {
|
||||
setError(t('usage.dataManagement.shrinkInvalid'));
|
||||
return;
|
||||
}
|
||||
const confirmed = await askConfirmation({
|
||||
title: t('usage.dataManagement.shrinkConfirmTitle'),
|
||||
message: t('usage.dataManagement.shrinkConfirm', { size: targetDatabaseSizeMb }),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
setShrinking(true);
|
||||
setError('');
|
||||
setStorageNotice('');
|
||||
try {
|
||||
const next = await invoke<UsageStorageSettings>('shrink_usage_database', { targetDatabaseSizeMb });
|
||||
setStorage(next);
|
||||
setStorageNotice(t(
|
||||
next.deletedRecords > 0
|
||||
? 'usage.dataManagement.shrinkSuccess'
|
||||
: 'usage.dataManagement.shrinkNoCleanup',
|
||||
{
|
||||
deleted: next.deletedRecords.toLocaleString(),
|
||||
size: formatStorageBytes(next.databaseSizeBytes),
|
||||
},
|
||||
));
|
||||
} catch (requestError) {
|
||||
setError(String(requestError));
|
||||
} finally {
|
||||
setShrinking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const repair = async () => {
|
||||
if (!await askConfirmation({ title: t('usage.dataManagement.title'), message: t('usage.dataManagement.confirm') })) return;
|
||||
setRunning(true);
|
||||
@@ -804,10 +840,7 @@ function UsageDataManagementView() {
|
||||
<div className="usage-data-management-heading">
|
||||
<div>
|
||||
<Wrench size={20} aria-hidden="true" />
|
||||
<div>
|
||||
<h2>{t('usage.dataManagement.title')}</h2>
|
||||
<p>{t('usage.dataManagement.description')}</p>
|
||||
</div>
|
||||
<h2>{t('usage.dataManagement.title')}</h2>
|
||||
</div>
|
||||
<span className="usage-data-management-badge">{t('usage.dataManagement.manualBadge')}</span>
|
||||
</div>
|
||||
@@ -828,15 +861,14 @@ function UsageDataManagementView() {
|
||||
<div className="usage-storage-limit-editor">
|
||||
<label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="1"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={limitDraft}
|
||||
disabled={loadingLimit || savingLimit}
|
||||
disabled={loadingLimit || savingLimit || shrinking || running}
|
||||
onChange={(event) => setLimitDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !loadingLimit && !savingLimit) void saveStorageLimit();
|
||||
if (event.key === 'Enter' && !loadingLimit && !savingLimit && !shrinking && !running) void saveStorageLimit();
|
||||
}}
|
||||
aria-label={t('usage.dataManagement.storageInput')}
|
||||
/>
|
||||
@@ -846,13 +878,45 @@ function UsageDataManagementView() {
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={() => void saveStorageLimit()}
|
||||
disabled={loadingLimit || savingLimit}
|
||||
disabled={loadingLimit || savingLimit || shrinking || running}
|
||||
>
|
||||
{savingLimit ? t('usage.dataManagement.storageSaving') : t('usage.dataManagement.storageSave')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="usage-data-management-action">
|
||||
<div>
|
||||
<strong>{t('usage.dataManagement.shrinkTitle')}</strong>
|
||||
<span>{t('usage.dataManagement.shrinkDescription')}</span>
|
||||
</div>
|
||||
<div className="usage-storage-limit-editor">
|
||||
<label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
value={shrinkDraft}
|
||||
disabled={savingLimit || shrinking || running}
|
||||
onChange={(event) => setShrinkDraft(event.currentTarget.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter' && !savingLimit && !shrinking && !running) void shrinkDatabase();
|
||||
}}
|
||||
aria-label={t('usage.dataManagement.shrinkInput')}
|
||||
/>
|
||||
<span>{t('usage.dataManagement.storageUnit')}</span>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={() => void shrinkDatabase()}
|
||||
disabled={savingLimit || shrinking || running}
|
||||
>
|
||||
{shrinking ? t('usage.dataManagement.shrinking') : t('usage.dataManagement.shrinkRun')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{storageNotice ? <MessageNotice tone="success" message={storageNotice} onDismiss={() => setStorageNotice('')} /> : null}
|
||||
|
||||
<div className="usage-data-management-action">
|
||||
@@ -860,7 +924,7 @@ function UsageDataManagementView() {
|
||||
<strong>{t('usage.dataManagement.actionTitle')}</strong>
|
||||
<span>{t('usage.dataManagement.actionDescription')}</span>
|
||||
</div>
|
||||
<button type="button" className="primary-button" onClick={() => void repair()} disabled={running}>
|
||||
<button type="button" className="primary-button" onClick={() => void repair()} disabled={running || savingLimit || shrinking}>
|
||||
{running ? t('usage.dataManagement.running') : t('usage.dataManagement.run')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+3
-1
@@ -9399,7 +9399,9 @@ body.table-col-resizing * {
|
||||
}
|
||||
|
||||
.usage-storage-limit-editor input {
|
||||
width: 104px;
|
||||
width: 168px;
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
||||
Reference in new issue
Block a user