Files
Oxicloud/frontend/src/routes/+layout.svelte
T
DioCrafts db97a88956 feat(upload): skip unreadable files (FIFOs/sockets) + auto-reload on new deploy
Two robustness fixes behind the recurring "folder upload stuck at ~93%" reports.

1. Skip non-regular files up front. A copied s6/runit service tree contains
   FIFOs (e.g. supervise/control named pipes) that report a size but BLOCK
   FOREVER when the browser reads them — the deterministic ~8-files-short that
   no retry/watchdog tweak could fix. uploadBatch/uploadTree now probe each
   file's first chunk against a 3 s timeout (partitionReadable), upload only the
   readable ones, and report the rest: "N uploaded · M skipped (not regular
   files)". Progress runs over the uploadable count, so it reaches 100% instead
   of parking at 93% while a lane hangs on a pipe.

2. Auto-reload on a new deploy. svelte.config.js polls _app/version.json
   (60 s); the root layout reloads itself when the deployed build changes —
   unless an upload is in flight — so an open tab can't keep running stale code
   after a rebuild (the recurring "my fix isn't applied" trap).

npm run check: 0 errors, 58 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:08:29 +02:00

82 lines
2.6 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { page, updated } from '$app/state';
import { onMount } from 'svelte';
import '$lib/styles/app.css';
import AppShell from '$lib/components/AppShell.svelte';
import DialogHost from '$lib/components/DialogHost.svelte';
import Toaster from '$lib/components/Toaster.svelte';
import { session } from '$lib/stores/session.svelte';
import { ui } from '$lib/stores/ui.svelte';
import { hashUrlToPath } from '$lib/utils/hashRedirect';
let { children } = $props();
// A new build was deployed (`_app/version.json` changed, polled per
// svelte.config.js) — reload so an open tab never keeps running stale code
// after a rebuild. Deferred while a progress notification (e.g. an upload) is
// in flight so we don't interrupt it; this effect re-runs when that clears
// (notifications are reactive) and reloads then.
$effect(() => {
if (!updated.current) return;
const busy = ui.notifications.some((n) => n.progress !== undefined);
if (!busy) location.reload();
});
// Routes reachable without an authenticated session.
const PUBLIC_PREFIXES = ['/login', '/device', '/s/', '/nextcloud'];
function isPublic(pathname: string): boolean {
return PUBLIC_PREFIXES.some((p) => pathname === p || pathname.startsWith(p));
}
let ready = $state(false);
onMount(async () => {
// The instant HTML boot splash has done its job — the app is mounted, so
// the route (login renders immediately; protected routes show their own
// loading state) is already in the DOM behind it.
document.getElementById('app-splash')?.remove();
// Redirect old `#/...` bookmarks to the new path before anything else.
if (typeof location !== 'undefined' && location.hash.startsWith('#/')) {
const mapped = hashUrlToPath(location.hash);
if (mapped) await goto(mapped, { replaceState: true });
}
await session.load();
ready = true;
});
// Guard: once the session is known, bounce unauthenticated users off
// protected routes. Runs client-side only (ssr=false).
$effect(() => {
if (!ready) return;
const path = page.url.pathname;
if (!session.isAuthenticated && !isPublic(path)) {
void goto(`/login?redirect=${encodeURIComponent(path)}`, { replaceState: true });
}
});
</script>
{#if isPublic(page.url.pathname)}
{@render children()}
{:else if ready && session.isAuthenticated}
<AppShell {children} />
{:else if ready}
{@render children()}
{:else}
<div class="app-loading" aria-busy="true">Loading…</div>
{/if}
<Toaster />
<DialogHost />
<style>
.app-loading {
display: grid;
place-items: center;
min-height: 100vh;
color: var(--color-text-muted);
}
</style>