feat: delta/instant upload + frontend UI/UX polish

Bundles the backend+frontend delta-upload (content-dedup) feature with a
batch of frontend fixes from this session.

Upload / dedup:
- Client-hashed delta & instant upload (deltaUpload, hashWasm vendor shim)
- Backend dedup batch endpoint (dedup_service, dedup_handler, routes)
- session store owned-hash helpers; unit tests + upload-strategy bench

Frontend UI/UX:
- Colour file-type icons in grid/list (per-type tinted tiles + glyph hue)
- Robust thumbnail fallback; PDFs now show their type icon (backend
  generates no PDF thumbnails) instead of a blank tile
- Fix PDF preview: load via a same-origin blob: iframe — the API URL is
  blocked by the global X-Frame-Options: DENY in the browser's framed
  PDF viewer, matching the existing CSP `frame-src blob:` design
- Groups: localized virtual-group description (no DB schema-note leak),
  add nav.groups to the 15 missing locales, fix primary-button contrast
- Repoint --color-text-light → --color-on-accent (was faint grey on accent)
- Nudge the admin role badge off the user-menu header divider

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-20 16:33:08 +02:00
parent f8490ad96e
commit d98e3117b2
35 changed files with 901 additions and 101 deletions
@@ -0,0 +1,45 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { User } from '$lib/api/types';
// `vi.mock` is hoisted above imports, so the spy it references must be created
// with `vi.hoisted` (a plain top-level const isn't initialised yet when the
// factory runs).
const { fetchMeMock } = vi.hoisted(() => ({ fetchMeMock: vi.fn() }));
vi.mock('$lib/api/endpoints/auth', () => ({
fetchMe: () => fetchMeMock(),
tryRefresh: vi.fn(async () => false)
}));
import { session } from './session.svelte';
const userWithUsage = (used: number) => ({ storage_used_bytes: used }) as unknown as User;
describe('session.refresh', () => {
beforeEach(() => {
fetchMeMock.mockReset();
session.reset();
});
it('pulls the fresh storage usage into the reactive user (upload/delete sync)', async () => {
fetchMeMock.mockResolvedValue(userWithUsage(2048));
await session.refresh();
expect(session.user?.storage_used_bytes).toBe(2048);
});
it('leaves the current user intact when the probe returns null', async () => {
fetchMeMock.mockResolvedValue(userWithUsage(2048));
await session.refresh();
fetchMeMock.mockResolvedValue(null);
await session.refresh();
expect(session.user?.storage_used_bytes).toBe(2048);
});
it('leaves the current user intact when the probe throws', async () => {
fetchMeMock.mockResolvedValue(userWithUsage(2048));
await session.refresh();
fetchMeMock.mockRejectedValue(new Error('network'));
await session.refresh();
expect(session.user?.storage_used_bytes).toBe(2048);
});
});
+16
View File
@@ -40,6 +40,22 @@ class SessionStore {
return this.user;
}
/**
* Re-fetch the authenticated user from the server, bypassing the one-shot
* `load()` cache. Call after operations that change server-side user state —
* chiefly storage usage after uploads / deletes — so the UI reflects the new
* `storage_used_bytes` instead of the value cached at login. A transient
* failure leaves the current user untouched (never logs the UI out).
*/
async refresh(): Promise<void> {
try {
const me = await fetchMe();
if (me) this.user = me;
} catch {
/* keep the existing user on a transient /api/auth/me failure */
}
}
/**
* Resolve the caller's default personal drive's root folder — the landing
* point for `/files` and the `/` redirect. Externals (grant-only) have no