feat: setup logs retention by max file size

This commit is contained in:
fsylvain committed 2026-10-07 22:02:13 +02:00
1 parent 70c8492f4f
commit f914eae3c5
44 files changed
+2316 -161

No files matched your search

@@ -3,8 +3,11 @@ import { initializeLocale, setLocale } from '@/i18n'
import type { WorkspaceSettings } from '@/composables/useSettings'
import { recentWhenLabel } from '../activityTime'
import {
formatLogSize,
helperCopy,
isShorter,
isSmaller,
sizeHelperCopy,
lastCleanupLine,
nextCleanupLine,
shorteningDescription,
@@ -20,6 +23,8 @@ function settings(overrides: Partial<WorkspaceSettings> = {}): WorkspaceSettings
return {
version: 'v2.2.0-beta',
activity_retention_days: 90,
activity_retention_max_gb: 0,
activity_log_size_bytes: 0,
activity_last_cleanup_at: at(6, 3, 0),
activity_last_cleanup_removed: 142,
activity_next_cleanup_at: at(7, 3, 0),
@@ -86,3 +91,43 @@ describe('retention lines', () => {
)
})
})
describe('retention size limit', () => {
it('ranks 1 < 5 < 10 < 25 < 50 < No size limit', () => {
expect(isSmaller(1, 5)).toBe(true)
expect(isSmaller(5, 0)).toBe(true)
expect(isSmaller(0, 50)).toBe(false)
expect(isSmaller(5, 5)).toBe(false)
expect(isSmaller(10, 50)).toBe(true)
})
it.each([
[0, 'Less than 0.1 GB'],
[53687091, 'Less than 0.1 GB'],
[107374183, '0.1 GB'],
[1073741824, '1.0 GB'],
[3435973837, '3.2 GB'],
[5368709120, '5.0 GB'],
])('formats %s bytes as %s on a 1024^3 base', (bytes, shown) => {
expect(formatLogSize(bytes)).toBe(shown)
})
it('words the helpers for each combination', () => {
expect(helperCopy(90, 5)).toBe('Events older than this are removed.')
expect(helperCopy(0, 5)).toBe('No age limit. Only the size limit applies.')
expect(helperCopy(0, 0)).toBe('Nothing is removed automatically. The log grows over time.')
expect(sizeHelperCopy(0, 99)).toBe('Optional. Logs can grow without a size cap.')
expect(sizeHelperCopy(5, 3435973837)).toBe(
'When logs pass 5 GB, the oldest events are removed. Currently 3.2 GB.',
)
})
it('words the helpers in French with Go and a decimal comma', () => {
setLocale('fr')
expect(sizeHelperCopy(5, 3435973837)).toBe(
'Au-delà de 5 Go, les événements les plus anciens sont supprimés. Actuellement 3,2 Go.',
)
expect(formatLogSize(0)).toBe('Moins de 0,1 Go')
})
})
@@ -101,6 +101,26 @@ describe('Sentences: English and French come from the same code and params', ()
'Changed the activity retention from 90 days to Forever',
"Durée de conservation de l'activité modifiée : 90 jours → Toujours",
],
[
'system.activity_retention_changed size only',
{
action: 'system.activity_retention_changed',
target: null,
params: { max_gb_from: 0, max_gb_to: 5 },
},
'Changed the activity size limit from No size limit to 5 GB',
"Limite de taille de l'activité modifiée : Aucune limite de taille → 5 Go",
],
[
'system.activity_retention_changed age and size',
{
action: 'system.activity_retention_changed',
target: null,
params: { from: 90, to: 30, max_gb_from: 10, max_gb_to: 0 },
},
'Changed the activity retention from 90 days to 30 days and the size limit from 10 GB to No size limit',
"Durée de conservation de l'activité modifiée : 90 jours → 30 jours, et limite de taille : 10 Go → Aucune limite de taille",
],
[
'system.restart_requested update',
{
+49 -4
View File
@@ -1,11 +1,16 @@
import { t } from '@/i18n'
import { formatNumber, t } from '@/i18n'
import type { WorkspaceSettings } from '@/composables/useSettings'
import { localDayTime } from '@/lib/activityTime'
export const FOREVER_DAYS = 0
export const NO_SIZE_LIMIT = 0
export const SAVED_VISIBLE_MS = 2000
const BYTES_PER_GB = 1024 ** 3
const MIN_SHOWN_GB = 0.1
const RANKED_PERIODS = [30, 90, 365, FOREVER_DAYS]
const RANKED_SIZES = [1, 5, 10, 25, 50, NO_SIZE_LIMIT]
export function retentionOptions() {
return RANKED_PERIODS.map((value) => ({ value, label: periodLabel(value) }))
@@ -32,12 +37,52 @@ export function isShorter(candidate: number, saved: number) {
return periodRank(candidate) < periodRank(saved)
}
export function helperCopy(days: number) {
return days === FOREVER_DAYS
? t('Nothing is removed automatically. The log grows over time.')
export function sizeOptions() {
return RANKED_SIZES.map((value) => ({ value, label: sizeLabel(value) }))
}
export function sizeLabel(gb: number) {
return gb === NO_SIZE_LIMIT ? t('No size limit') : t('{0} GB', [gb])
}
export function sizeRank(gb: number) {
return RANKED_SIZES.indexOf(gb)
}
export function isSmaller(candidate: number, saved: number) {
return sizeRank(candidate) < sizeRank(saved)
}
export function formatLogSize(bytes: number) {
const gb = bytes / BYTES_PER_GB
if (gb < MIN_SHOWN_GB) {
return t('Less than 0.1 GB')
}
const figure = formatNumber(gb, { minimumFractionDigits: 1, maximumFractionDigits: 1 })
return t('{0} GB', [figure])
}
export function helperCopy(days: number, gb = NO_SIZE_LIMIT) {
const sized = gb !== NO_SIZE_LIMIT
if (days === FOREVER_DAYS) {
return sized
? t('No age limit. Only the size limit applies.')
: t('Nothing is removed automatically. The log grows over time.')
}
return sized
? t('Events older than this are removed.')
: t('Older events are removed automatically.')
}
export function sizeHelperCopy(gb: number, bytes: number) {
return gb === NO_SIZE_LIMIT
? t('Optional. Logs can grow without a size cap.')
: t('When logs pass {0} GB, the oldest events are removed. Currently {1}.', [
gb,
formatLogSize(bytes),
])
}
export function lastCleanupLine(settings: WorkspaceSettings) {
if (!settings.activity_last_cleanup_at) {
return t('Never')
+29 -1
View File
@@ -1,6 +1,7 @@
import { formatNumber, t } from '@/i18n'
import type { MessageKey } from '@/i18n/messages'
import type { ActivityItem } from '@/lib/activity'
import { sizeLabel } from '@/lib/activityRetention'
export interface SentenceSegment {
text: string
@@ -139,9 +140,36 @@ function retentionDays(value: unknown) {
return days === 0 ? t('Forever') : t('{0} days', [formatNumber(days)])
}
function changedAge(params: Record<string, unknown>) {
return 'from' in params || 'to' in params
}
function changedSize(params: Record<string, unknown>) {
return 'max_gb_from' in params || 'max_gb_to' in params
}
function retentionTemplate(params: Record<string, unknown>): MessageKey {
if (changedSize(params) && changedAge(params)) {
return 'Changed the activity retention from {0} to {1} and the size limit from {2} to {3}'
}
return changedSize(params)
? 'Changed the activity size limit from {0} to {1}'
: 'Changed the activity retention from {0} to {1}'
}
function retentionValues(params: Record<string, unknown>) {
const sizes = [sizeLabel(count(params.max_gb_from)), sizeLabel(count(params.max_gb_to))]
if (!changedAge(params)) return sizes
const ages = [retentionDays(params.from), retentionDays(params.to)]
return changedSize(params) ? [...ages, ...sizes] : ages
}
function resolveTemplate(item: ActivityItem): MessageKey | null {
const template = CATALOGUE[item.action]
if (!template) return null
if (item.action === 'system.activity_retention_changed') {
return retentionTemplate(item.params)
}
if (item.action === 'system.restart_requested' && item.outcome === 'success') {
return item.params.kind === 'update'
? 'Restarted to apply the update {0}'
@@ -156,7 +184,7 @@ function slotValues(item: ActivityItem, template: MessageKey) {
return [purgeSummary(item.params)]
}
if (item.action === 'system.activity_retention_changed') {
return [retentionDays(item.params.from), retentionDays(item.params.to)]
return retentionValues(item.params)
}
if (item.action === 'system.activity_cleanup') {
return [formatNumber(count(item.params.removed))]