No backup runs yet.
Run a backup — its history appears here.
@@ -99,13 +99,13 @@
{@const isFail = exec.status === 'failed'}
{@const isOpen = expanded.has(exec.id)}
- | {exec.triggeredAt ? formatDateTime(exec.triggeredAt, true) : '—'} |
+ {#if exec.triggeredAt}{formatDateTime(exec.triggeredAt, true)} ({formatRelativeTime(exec.triggeredAt)}){:else}—{/if} |
|
- {formatDuration(exec.duration)} |
+ {formatDuration(exec.duration)} |
diff --git a/src/lib/components/ui/loading-state/index.ts b/src/lib/components/ui/loading-state/index.ts
new file mode 100644
index 0000000..c2e687f
--- /dev/null
+++ b/src/lib/components/ui/loading-state/index.ts
@@ -0,0 +1,3 @@
+import LoadingState from './loading-state.svelte';
+
+export { LoadingState };
diff --git a/src/lib/components/ui/loading-state/loading-state.svelte b/src/lib/components/ui/loading-state/loading-state.svelte
new file mode 100644
index 0000000..69a3828
--- /dev/null
+++ b/src/lib/components/ui/loading-state/loading-state.svelte
@@ -0,0 +1,22 @@
+
+
+
+
+ {#if label}
+ {label}
+ {/if}
+
diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte
index d1772f2..990235a 100644
--- a/src/lib/components/ui/tooltip/tooltip-content.svelte
+++ b/src/lib/components/ui/tooltip/tooltip-content.svelte
@@ -21,7 +21,7 @@
{sideOffset}
{side}
class={cn(
- "bg-popover text-popover-foreground border shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin) z-[200] w-fit fixed text-balance rounded-md px-3 py-1.5 text-xs",
+ "bg-popover text-popover-foreground border shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-end-2 data-[side=right]:slide-in-from-start-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-tooltip-content-transform-origin) z-[200] w-max max-w-lg fixed whitespace-normal rounded-md px-3 py-1.5 text-xs",
className
)}
{...restProps}
diff --git a/src/lib/utils/format.ts b/src/lib/utils/format.ts
index bedf6e2..b789be7 100644
--- a/src/lib/utils/format.ts
+++ b/src/lib/utils/format.ts
@@ -17,3 +17,40 @@ export function formatBytesCompact(bytes: number, decimals = 1): string {
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : decimals)}${units[i]}`;
}
+
+/**
+ * Make a long single-line error readable by putting each `[NNNN]` log marker on
+ * its own line. Scanner/CLI output (grype, trivy, restic) crams a whole log
+ * stream into one line prefixed by numeric markers like `[0026]`; splitting on
+ * those turns the wall of text into one line per log entry.
+ *
+ * Only NUMERIC brackets are split — `[]` (an empty compose field path such as
+ * `environment.[]`) is left attached so a field name is never torn in half.
+ */
+export function formatErrorLines(msg: string | null | undefined): string {
+ if (!msg) return '';
+ return msg.replace(/\s*(\[\d+\])/g, '\n$1').trim();
+}
+
+/**
+ * Compact relative time like "just now", "5m ago", "2d ago", "3mo ago". Returns ''
+ * for an unparseable/NaN date and "in the future" for a timestamp ahead of now.
+ * Kept in this import-light module so it is unit-testable; stores/settings re-exports it.
+ */
+export function formatRelativeTime(date: Date | string | number): string {
+ const d = date instanceof Date ? date : new Date(date);
+ const ms = d.getTime();
+ if (isNaN(ms)) return '';
+ const sec = Math.round((Date.now() - ms) / 1000);
+ if (sec < 0) return 'in the future';
+ if (sec < 45) return 'just now';
+ const min = Math.round(sec / 60);
+ if (min < 60) return `${min}m ago`;
+ const hr = Math.round(min / 60);
+ if (hr < 24) return `${hr}h ago`;
+ const day = Math.round(hr / 24);
+ if (day < 30) return `${day}d ago`;
+ const mo = Math.round(day / 30);
+ if (mo < 12) return `${mo}mo ago`;
+ return `${Math.round(mo / 12)}y ago`;
+}
diff --git a/src/routes/containers/FileBrowserPanel.svelte b/src/routes/containers/FileBrowserPanel.svelte
index 3e23c08..4a6ffe8 100644
--- a/src/routes/containers/FileBrowserPanel.svelte
+++ b/src/routes/containers/FileBrowserPanel.svelte
@@ -38,6 +38,7 @@
TextCursorInput
} from 'lucide-svelte';
import { toast } from 'svelte-sonner';
+ import { LoadingState } from '$lib/components/ui/loading-state';
import { formatDateTime, appSettings } from '$lib/stores/settings';
interface FileEntry {
@@ -762,13 +763,19 @@
currentPath = data.path || path;
// Map snapshot entries to match FileEntry interface
if (isSnapshotMode) {
+ // restic ls --json gives permissions as a 10-char ls string
+ // ("-rw-------": leading type char + 9 rwx). permissionsToOctal wants
+ // the 9 rwx chars, so drop the leading type char. Owner/group come as
+ // user/group names when the source had /etc/passwd, else numeric uid/gid.
entries = (data.entries || []).map((e: any) => ({
name: e.name,
type: e.type === 'dir' ? 'directory' : e.type,
size: e.size || 0,
- permissions: '',
- owner: '',
- group: '',
+ permissions: typeof e.permissions === 'string' && e.permissions.length >= 10
+ ? e.permissions.slice(1)
+ : '',
+ owner: e.user || (e.uid != null ? String(e.uid) : ''),
+ group: e.group || (e.gid != null ? String(e.gid) : ''),
modified: e.mtime || ''
}));
} else {
@@ -1002,10 +1009,7 @@
{#if loading}
-
-
- Loading...
-
+
{/if}
{#if error}
@@ -1023,8 +1027,12 @@
{showHiddenFiles ? 'Directory is empty' : 'No visible files (hidden files are hidden)'}
{:else if displayEntries().length > 0}
-
-
+
+
+
-
+
Permissions
+
+ Owner
+
- Actions
+ Actions
@@ -1093,10 +1104,17 @@
{entry.type === 'directory' ? '-' : formatSize(entry.size)}
-
+
{permissionsToOctal(entry.permissions)}
{entry.permissions}
+
+ {#if entry.owner}
+ {entry.owner}{#if entry.group && entry.group !== entry.owner}:{entry.group}{/if}
+ {:else}
+ -
+ {/if}
+
{formatDate(entry.modified)}
@@ -1184,7 +1202,7 @@
{/each}
-
+
{/if}
@@ -1250,10 +1268,7 @@
{#if loadingPreview && !viewingFile}
-
-
- Loading preview{previewingName ? ` — ${previewingName}` : ''}…
-
+
{:else if viewingFile}
diff --git a/src/routes/images/VulnerabilityScanModal.svelte b/src/routes/images/VulnerabilityScanModal.svelte
index 9bda455..034345d 100644
--- a/src/routes/images/VulnerabilityScanModal.svelte
+++ b/src/routes/images/VulnerabilityScanModal.svelte
@@ -1,4 +1,5 @@
|