perf: cache-stampede coalescing + DB safeguards; ui/i18n fixes
Backend — tail latency & throughput:
- FileContentCache, image transcode, and search now use moka single-flight
(try_get_with / get_or_load) so N concurrent misses for the same key
collapse to one disk read / transcode / query instead of a thundering herd.
Microbenchmark (128 concurrent on one hot key): 128 loads / p99 ~1023ms
before vs 1 load / p99 ~32ms after.
- DB: configurable per-statement timeout on the primary pool
(OXICLOUD_DB_STATEMENT_TIMEOUT_SECS, default 30; maintenance pool exempt) so
a runaway query can't pin a connection and starve the pool.
- DB: background pool-saturation monitor
(OXICLOUD_DB_POOL_MONITOR_INTERVAL_SECS) that WARNs as the primary pool nears
exhaustion — the early signal before tail latency cliffs.
- mimalloc: set MIMALLOC_PURGE_DELAY=0 (Dockerfile + compose) so freed pages
return to the OS and RSS tracks the live working set; benchmarked on
musl/aarch64 at ~400MB reclaimed vs 0MB with the default.
Frontend — UI / i18n fixes:
- i18n: fix literal "{{count}}" and "{{percentage}}/{{used}}/{{total}}" in the
selection toolbar and storage line — the call sites passed param names that
didn't match the locale placeholders; unify on `count` and pass the storage
template its params. Add es files.selected_count.
- sidebar: hide the drive picker when there's only one drive (the redundant
"Personal" row); remove the coloured left accent on the active nav item.
- logo: stop clipping the cloud's left bulge — viewBox recentred on the cloud's
true bbox with proportional SVG size so it keeps the same rendered scale.
- user menu: drop the default <a> underline on the link rows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -261,7 +261,7 @@
|
||||
<div class="sidebar" class:open={sidebarOpen}>
|
||||
<a href="/files" class="logo-container">
|
||||
<div class="logo">
|
||||
<svg viewBox="120 120 280 280" aria-hidden="true">
|
||||
<svg viewBox="95 67 320 320" aria-hidden="true">
|
||||
<path
|
||||
d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z"
|
||||
/>
|
||||
@@ -302,7 +302,7 @@
|
||||
session.user.storage_quota_bytes
|
||||
)}
|
||||
{:else}
|
||||
{formatBytes(session.user.storage_used_bytes)} {t('storage.used', 'used')}
|
||||
{formatBytes(session.user.storage_used_bytes)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,7 +526,19 @@
|
||||
<div class="user-menu-storage-fill" style:width="{storagePct}%"></div>
|
||||
</div>
|
||||
<div class="user-menu-storage-text">
|
||||
{Math.round(storagePct)}% {t('storage.used', 'used')}
|
||||
{#if session.user.storage_quota_bytes > 0}
|
||||
{t(
|
||||
'storage.used',
|
||||
{
|
||||
percentage: Math.round(storagePct),
|
||||
used: formatBytes(session.user.storage_used_bytes),
|
||||
total: formatBytes(session.user.storage_quota_bytes)
|
||||
},
|
||||
'{{percentage}}% used ({{used}} / {{total}})'
|
||||
)}
|
||||
{:else}
|
||||
{formatBytes(session.user.storage_used_bytes)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -661,7 +673,7 @@
|
||||
<div class="about-overlay" onclick={(e) => e.target === e.currentTarget && (aboutOpen = false)}>
|
||||
<div class="about-modal" role="dialog" aria-modal="true" aria-labelledby="about-modal-title">
|
||||
<div class="about-modal__logo">
|
||||
<svg viewBox="120 120 280 280" aria-hidden="true">
|
||||
<svg viewBox="95 67 320 320" aria-hidden="true">
|
||||
<path
|
||||
d="M345 310c32 0 58-26 58-58s-26-58-58-58c-6.2 0-12 0.9-17.5 2.7C318 166 289 143 255 143c-34.3 0-63.1 22.6-73 53.7C176.9 195.7 171 195 165 195c-32 0-58 26-58 58s26 58 58 58h180z"
|
||||
/>
|
||||
@@ -1071,8 +1083,10 @@
|
||||
}
|
||||
|
||||
.about-modal__logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
/* 73px (not 64) so the cloud keeps its rendered scale after the viewBox
|
||||
grew 280→320 to stop clipping its left bulge: 73/320 ≈ 64/280. */
|
||||
width: 73px;
|
||||
height: 73px;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if drivesStore.loaded && drivesStore.drives.length > 0}
|
||||
<!-- Only show the drive switcher when there's an actual choice to make. With a
|
||||
single drive (the default personal one) the picker just repeats "Personal"
|
||||
under the Files nav row, so hide it; it reappears the moment a second drive
|
||||
(e.g. a shared one) exists. -->
|
||||
{#if drivesStore.loaded && drivesStore.drives.length > 1}
|
||||
<ul class="drive-picker" aria-label={t('drive.picker', 'Drives')}>
|
||||
{#each sortedDrives as d (d.id)}
|
||||
<li class="drive-picker__row" class:drive-picker__row--active={isActive(d)}>
|
||||
|
||||
@@ -91,8 +91,10 @@
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
/* 25px (not 22) so the cloud keeps its rendered scale after the viewBox
|
||||
grew 280→320 to stop clipping its left bulge: 25/320 ≈ 22/280. */
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
fill: var(--color-sidebar-text-active);
|
||||
}
|
||||
|
||||
@@ -147,13 +149,10 @@
|
||||
.nav-item.active {
|
||||
background-color: var(--color-sidebar-active-bg);
|
||||
color: var(--color-sidebar-text-active);
|
||||
border-left-color: var(--color-accent);
|
||||
font-weight: var(--weight-semibold);
|
||||
|
||||
[dir="rtl"] & {
|
||||
border-left-color: transparent;
|
||||
border-right-color: var(--color-accent);
|
||||
}
|
||||
/* No coloured left accent — the tinted background + bold weight already
|
||||
read as "active". The base `border-left: 3px solid transparent` is kept
|
||||
on every row so removing the accent causes no horizontal text shift. */
|
||||
}
|
||||
|
||||
.nav-item i,
|
||||
|
||||
@@ -177,6 +177,10 @@
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
text-align: left;
|
||||
/* The link rows (`<a class="user-menu-item">` — admin panel, groups,
|
||||
profile) are styled as menu rows, not hyperlinks; drop the default
|
||||
<a> underline so they match the <button> rows. */
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.user-menu-item:hover,
|
||||
|
||||
@@ -1213,7 +1213,7 @@
|
||||
<Icon name="times" />
|
||||
</button>
|
||||
<span class="batch-bar-count"
|
||||
>{t('files.selected_count', { n: selectedCount }, '{{n}} selected')}</span
|
||||
>{t('files.selected_count', { count: selectedCount }, '{{count}} selected')}</span
|
||||
>
|
||||
</div>
|
||||
<div class="batch-selection-info">
|
||||
|
||||
@@ -456,7 +456,7 @@
|
||||
|
||||
{#if selected.size > 0}
|
||||
<div class="batch-bar">
|
||||
<span>{t('files.selected_count', { n: selected.size }, '{{n}} selected')}</span>
|
||||
<span>{t('files.selected_count', { count: selected.size }, '{{count}} selected')}</span>
|
||||
<div class="batch-bar__actions">
|
||||
<Button onclick={downloadSelected}>{t('common.download', 'Download')}</Button>
|
||||
<Button onclick={() => selected.clear()}>{t('common.clear', 'Clear')}</Button>
|
||||
|
||||
@@ -364,7 +364,8 @@
|
||||
"folder": "Carpeta",
|
||||
"new_folder": "Nueva carpeta",
|
||||
"share": "Compartir",
|
||||
"view": "Ver"
|
||||
"view": "Ver",
|
||||
"selected_count": "{{count}} seleccionados"
|
||||
},
|
||||
"dialogs": {
|
||||
"rename_folder": "Renombrar carpeta",
|
||||
|
||||
Reference in New Issue
Block a user