feat(storage key rot): add admin panel
This commit is contained in:
@@ -470,7 +470,13 @@
|
||||
return name_is_recoverable(job.name);
|
||||
}
|
||||
function name_is_recoverable(name: string): boolean {
|
||||
return name.endsWith('_consistency') || name === 'storage_migration';
|
||||
// K3 storage-key-rotation adds `storage_rotate` to the recoverable
|
||||
// tenant set. Same shape as `storage_migration` — walks blobs,
|
||||
// records findings, supports resume from cursor — so it needs the
|
||||
// same expand/runs/findings surface.
|
||||
return (
|
||||
name.endsWith('_consistency') || name === 'storage_migration' || name === 'storage_rotate'
|
||||
);
|
||||
}
|
||||
|
||||
// Consistency batch shortcut — top button. Only shown when the
|
||||
|
||||
@@ -1041,6 +1041,13 @@
|
||||
different copy. -->
|
||||
{#if serverStatus().readonly}
|
||||
<ReadOnlyBanner variant="maintenance" progress={serverStatus().migration} />
|
||||
{:else if serverStatus().rotation}
|
||||
<!-- K4 storage-key-rotation: rotation is running but
|
||||
`readonly` is false — writes continue as normal.
|
||||
Distinct banner variant so the copy reads
|
||||
"background maintenance" rather than "server
|
||||
frozen". -->
|
||||
<ReadOnlyBanner variant="rotating" progress={serverStatus().rotation} />
|
||||
{/if}
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
@@ -1,37 +1,27 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Read-only banner — one component, two variants.
|
||||
* Read-only banner — one component, three variants.
|
||||
*
|
||||
* ## `variant="drive"` (default) — drive-scoped freeze
|
||||
*
|
||||
* Rendered at the top of any page whose content lives in (or is scoped
|
||||
* to) a drive whose `policies.read_only === true`. Members see the
|
||||
* banner and understand why upload / rename / delete / share
|
||||
* affordances elsewhere in the app fail with a generic error toast —
|
||||
* the backend engine gate refuses every non-`Read` permission on
|
||||
* resources in the drive.
|
||||
*
|
||||
* Only `Read` permissions pass; the banner does not need to gate any
|
||||
* behavior itself. It's pure signage. Backed by
|
||||
* `docs/plan/drive.md` §8 (`read_only`).
|
||||
*
|
||||
* Consumed by:
|
||||
* - `routes/config/drive/[uuid]/+page.svelte` — always shown when
|
||||
* the drive being configured is frozen.
|
||||
* - `routes/files/[...path]/+page.svelte` — shown when the current
|
||||
* folder's owning drive is frozen (parent looks up drive via
|
||||
* `drives.findByRootFolderId`/`findById`).
|
||||
* to) a drive whose `policies.read_only === true`.
|
||||
*
|
||||
* ## `variant="maintenance"` — server-wide freeze
|
||||
*
|
||||
* Rendered inside `AppShell` above `{children}` when the
|
||||
* `x-server-status` header (see `middleware::server_status`) says
|
||||
* the whole server is in read-only mode — typically during a
|
||||
* storage-backend migration. Optional `progress` lets the banner
|
||||
* show target + percentage.
|
||||
* `x-server-status` header says the whole server is in read-only
|
||||
* mode — typically during a `storage_migration` cutover.
|
||||
*
|
||||
* Shape / accent is identical between both variants — the design
|
||||
* system reads them as the same family. Only the copy differs.
|
||||
* ## `variant="rotating"` — background key rotation
|
||||
*
|
||||
* K4 storage-key-rotation. `storage_rotate` walks blobs in place;
|
||||
* writes/reads continue normally throughout. Copy makes it clear
|
||||
* this is a background maintenance banner, not a freeze — the app
|
||||
* is fully usable.
|
||||
*
|
||||
* Shape / accent is identical across variants — the design system
|
||||
* reads them as the same family. Only the copy differs.
|
||||
*/
|
||||
import { t } from '$lib/i18n/index.svelte';
|
||||
import Icon from '$lib/icons/Icon.svelte';
|
||||
@@ -47,12 +37,13 @@
|
||||
/**
|
||||
* `"drive"` — a specific drive is frozen (default; back-compat
|
||||
* with pre-migration call sites). `"maintenance"` — the whole
|
||||
* server is in read-only mode.
|
||||
* server is in read-only mode. `"rotating"` — a background key
|
||||
* rotation is running; writes continue.
|
||||
*/
|
||||
variant?: 'drive' | 'maintenance';
|
||||
variant?: 'drive' | 'maintenance' | 'rotating';
|
||||
/** Drive-name shown in the body (variant="drive" only). */
|
||||
driveName?: string;
|
||||
/** Migration progress (variant="maintenance" only). */
|
||||
/** Migration/rotation progress (variant="maintenance" | "rotating" only). */
|
||||
progress?: Progress;
|
||||
}
|
||||
|
||||
@@ -61,19 +52,28 @@
|
||||
|
||||
<div
|
||||
class="read-only-banner"
|
||||
class:read-only-banner--rotating={variant === 'rotating'}
|
||||
role="region"
|
||||
aria-label={variant === 'maintenance'
|
||||
? t('server_status.readonly_banner_aria', 'Server maintenance in progress')
|
||||
: t('drive.read_only_banner.aria', 'This drive is read-only')}
|
||||
data-testid={variant === 'maintenance' ? 'server-status-banner' : 'read-only-banner'}
|
||||
: variant === 'rotating'
|
||||
? t('server_status.rotating_banner_aria', 'Storage key rotation in progress')
|
||||
: t('drive.read_only_banner.aria', 'This drive is read-only')}
|
||||
data-testid={variant === 'maintenance'
|
||||
? 'server-status-banner'
|
||||
: variant === 'rotating'
|
||||
? 'server-status-rotating-banner'
|
||||
: 'read-only-banner'}
|
||||
>
|
||||
<div class="read-only-banner__icon" aria-hidden="true">
|
||||
<Icon name="lock" />
|
||||
<Icon name={variant === 'rotating' ? 'key' : 'lock'} />
|
||||
</div>
|
||||
<div class="read-only-banner__body">
|
||||
<strong>
|
||||
{#if variant === 'maintenance'}
|
||||
{t('server_status.readonly_title', 'Server maintenance in progress')}
|
||||
{:else if variant === 'rotating'}
|
||||
{t('server_status.rotating_title', 'Storage key rotation in progress')}
|
||||
{:else if driveName}
|
||||
{t(
|
||||
'drive.read_only_banner.title_named',
|
||||
@@ -103,6 +103,24 @@
|
||||
'Uploads, renames, deletes, and shares are refused temporarily. Reads and downloads work as normal.'
|
||||
)}
|
||||
{/if}
|
||||
{:else if variant === 'rotating'}
|
||||
{#if progress}
|
||||
{t(
|
||||
'server_status.rotating_progress',
|
||||
{
|
||||
target: progress.target,
|
||||
migrated: progress.migrated,
|
||||
total: progress.total,
|
||||
percent: progress.percent
|
||||
},
|
||||
'Rotating encryption on `{{target}}` — {{percent}}% ({{migrated}} / {{total}} blobs). All operations continue normally; this is a background maintenance task.'
|
||||
)}
|
||||
{:else}
|
||||
{t(
|
||||
'server_status.rotating_body',
|
||||
'A background key rotation is normalising storage. All operations continue normally.'
|
||||
)}
|
||||
{/if}
|
||||
{:else}
|
||||
{t(
|
||||
'drive.read_only_banner.body',
|
||||
@@ -143,6 +161,18 @@
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
/* K4 rotating variant — same shape, info accent (softer than the
|
||||
default), signalling "background task, no user-facing freeze".
|
||||
Uses `--color-info` when the palette defines it, falls back to
|
||||
`--color-accent` otherwise. */
|
||||
.read-only-banner--rotating {
|
||||
border-left-color: var(--color-info, var(--color-accent));
|
||||
}
|
||||
|
||||
.read-only-banner--rotating .read-only-banner__icon {
|
||||
color: var(--color-info, var(--color-accent));
|
||||
}
|
||||
|
||||
.read-only-banner__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user