internal cleanups and build config
This commit is contained in:
8 files changed
+39
-31
No files matched your search
+6
-1
@@ -66,7 +66,12 @@ RUN apko build apko.yaml dockhand-base:latest output.tar \
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Stage 2: Application Builder (pure Node.js)
|
# Stage 2: Application Builder (pure Node.js)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
FROM --platform=$TARGETPLATFORM node:24-slim AS app-builder
|
# Pinned to node 24.18.1 (the exact node v1.0.40 shipped with). node 24.19.0 has a napi
|
||||||
|
# teardown bug that crashes the better-sqlite3 native addon during `npm run build`
|
||||||
|
# (Statement::~Statement -> Assert(env != nullptr), exit 134). The `24-slim` moving tag
|
||||||
|
# picked up 24.19.0, and once the build cache was pruned every build recompiled the addon
|
||||||
|
# on the broken node. Pinning the digest restores the known-good v1.0.40 toolchain.
|
||||||
|
FROM --platform=$TARGETPLATFORM node:24.18.1-slim@sha256:235600a8101ab264e117b1768e925532262668dc9b581ef1dd7d96ced463b8e7 AS app-builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
# (remote ARM login: pipe password over stdin, no quoted interpolation)
|
# (remote ARM login: pipe password over stdin, no quoted interpolation)
|
||||||
# (use write-capable dockerhub token so the Docker Hub push authorizes)
|
# (use write-capable dockerhub token so the Docker Hub push authorizes)
|
||||||
# (revert rclone — the rclone: backend is unfinished/removed; see vault rclone.md)
|
# (revert rclone — the rclone: backend is unfinished/removed; see vault rclone.md)
|
||||||
|
# (rebuild: registry GC orphaned a helper-image blob; force a clean multi-arch republish)
|
||||||
FROM alpine:3.21 AS os-builder
|
FROM alpine:3.21 AS os-builder
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|||||||
+4
-2
@@ -13,7 +13,9 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# IMPORTANT: Must use alpine builder so native addons (better-sqlite3) are
|
# IMPORTANT: Must use alpine builder so native addons (better-sqlite3) are
|
||||||
# compiled against musl libc, not glibc. Cross-ABI copies would not work.
|
# compiled against musl libc, not glibc. Cross-ABI copies would not work.
|
||||||
FROM node:24-alpine AS app-builder
|
# Pinned to 24.18.1 like the main Dockerfile: node 24.19.0 has a napi regression that
|
||||||
|
# aborts the vite build with "Assertion failed: (env) != nullptr" (SIGABRT).
|
||||||
|
FROM node:24.18.1-alpine AS app-builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ RUN cd collector && CGO_ENABLED=0 go build -o /app/bin/collection-worker .
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Stage 3: Final Image (Alpine-based runtime)
|
# Stage 3: Final Image (Alpine-based runtime)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
FROM node:24-alpine
|
FROM node:24.18.1-alpine
|
||||||
|
|
||||||
# Install runtime packages
|
# Install runtime packages
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
|
||||||
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
import * as Tooltip from "$lib/components/ui/tooltip/index.js";
|
||||||
import { cn, type WithElementRef } from "$lib/utils.js";
|
import { cn, type WithElementRef } from "$lib/utils.js";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
@@ -11,9 +10,18 @@
|
|||||||
|
|
||||||
const SIDEBAR_STORAGE_KEY = "sidebar:state";
|
const SIDEBAR_STORAGE_KEY = "sidebar:state";
|
||||||
|
|
||||||
|
// Read the persisted open/collapsed state SYNCHRONOUSLY (before first paint) so a
|
||||||
|
// collapsed sidebar doesn't flash open on every load. onMount ran too late — the
|
||||||
|
// initial render showed the default-open sidebar until localStorage was read.
|
||||||
|
function initialOpen(): boolean {
|
||||||
|
if (typeof localStorage === 'undefined') return true;
|
||||||
|
const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY);
|
||||||
|
return stored === null ? true : stored === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
open = $bindable(true),
|
open = $bindable(initialOpen()),
|
||||||
onOpenChange = () => {},
|
onOpenChange = () => {},
|
||||||
class: className,
|
class: className,
|
||||||
style,
|
style,
|
||||||
@@ -37,13 +45,6 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load state from localStorage on mount
|
|
||||||
onMount(() => {
|
|
||||||
const stored = localStorage.getItem(SIDEBAR_STORAGE_KEY);
|
|
||||||
if (stored !== null) {
|
|
||||||
open = stored === 'true';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
|
<svelte:window onkeydown={sidebar.handleShortcutKeydown} />
|
||||||
|
|||||||
@@ -166,12 +166,12 @@ export const repoSnapshotColumns: ColumnConfig[] = [
|
|||||||
|
|
||||||
// Backup destination grid columns
|
// Backup destination grid columns
|
||||||
export const backupDestinationColumns: ColumnConfig[] = [
|
export const backupDestinationColumns: ColumnConfig[] = [
|
||||||
{ id: 'type', label: 'Type', width: 80, minWidth: 60, resizable: false },
|
{ id: 'type', label: 'Type', sortable: true, sortField: 'type', width: 130, minWidth: 100, resizable: false },
|
||||||
{ id: 'name', label: 'Name', sortable: true, sortField: 'name', width: 180, minWidth: 120, grow: true },
|
{ id: 'name', label: 'Name', sortable: true, sortField: 'name', width: 180, minWidth: 120, grow: true },
|
||||||
{ id: 'repository', label: 'Repository', width: 280, minWidth: 150, grow: true },
|
{ id: 'repository', label: 'Repository', sortable: true, sortField: 'repository', width: 280, minWidth: 150, grow: true },
|
||||||
{ id: 'usage', label: 'Usage', width: 100, minWidth: 70 },
|
{ id: 'usage', label: 'Usage', width: 100, minWidth: 70 },
|
||||||
{ id: 'stats', label: 'Stats', width: 140, minWidth: 100 },
|
{ id: 'stats', label: 'Stats', width: 140, minWidth: 100 },
|
||||||
{ id: 'status', label: 'Status', width: 110, minWidth: 80 },
|
{ id: 'status', label: 'Status', sortable: true, sortField: 'status', width: 110, minWidth: 80 },
|
||||||
{ id: 'actions', label: '', fixed: 'end', width: 300, resizable: false }
|
{ id: 'actions', label: '', fixed: 'end', width: 300, resizable: false }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export type ExecutionStatus = 'queued' | 'running' | 'success' | 'warning' | 'fa
|
|||||||
* "failed" filter) speak `failed`. A concurrency reject is flagged by the caller
|
* "failed" filter) speak `failed`. A concurrency reject is flagged by the caller
|
||||||
* via `skipped` and lands as `skipped`. Keeping this in ONE named, testable place
|
* via `skipped` and lands as `skipped`. Keeping this in ONE named, testable place
|
||||||
* stops a failed backup from being written as `error` — a value the schedules UI
|
* stops a failed backup from being written as `error` — a value the schedules UI
|
||||||
* doesn't recognise, which made failed backups invisible under its filter.
|
* filter does not recognise.
|
||||||
*/
|
*/
|
||||||
export function toExecutionStatus(
|
export function toExecutionStatus(
|
||||||
terminalStatus: TerminalState['status'],
|
terminalStatus: TerminalState['status'],
|
||||||
@@ -72,17 +72,6 @@ export function toExecutionStatus(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The startup scrub: any operation still `running` when the process restarted
|
|
||||||
* could not have completed, so it is rewritten to `stale` (distinct from
|
|
||||||
* `error` — `error` means it ran and failed, `stale` means the process died and
|
|
||||||
* it should just be re-run). Returns the status to write, or null to leave the
|
|
||||||
* row untouched.
|
|
||||||
*/
|
|
||||||
export function scrubStatus(current: OperationStatus): OperationStatus | null {
|
|
||||||
return current === 'running' ? 'stale' : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounce decision for progress writes. Persisting every restic status line
|
* Debounce decision for progress writes. Persisting every restic status line
|
||||||
* would hammer the DB, so we write the first update immediately and then at most
|
* would hammer the DB, so we write the first update immediately and then at most
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface Environment {
|
|||||||
|
|
||||||
const STORAGE_KEY = 'dockhand:environment';
|
const STORAGE_KEY = 'dockhand:environment';
|
||||||
|
|
||||||
// Load initial state from localStorage
|
// Load initial state from localStorage (the last-used environment).
|
||||||
function getInitialEnvironment(): CurrentEnvironment | null {
|
function getInitialEnvironment(): CurrentEnvironment | null {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
@@ -131,7 +131,11 @@ function createEnvironmentsStore() {
|
|||||||
name: firstEnv.name
|
name: firstEnv.name
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(`[EnvStore] Current env ${currentId} still exists, keeping selection`);
|
// Keep selection; refresh its name from the list (covers a renamed env).
|
||||||
|
const match = data.find((e) => Number(e.id) === currentId);
|
||||||
|
if (match && match.name !== current!.name) {
|
||||||
|
currentEnvironment.set({ id: match.id, name: match.name });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clear environments on permission denied or other errors
|
// Clear environments on permission denied or other errors
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Loader2, CheckCircle, XCircle, Archive, Copy, Check, Square } from 'lucide-svelte';
|
import { Loader2, CheckCircle, XCircle, Archive, Copy, Check, Square } from 'lucide-svelte';
|
||||||
|
import type { Component } from 'svelte';
|
||||||
import LogConsole from '$lib/components/LogConsole.svelte';
|
import LogConsole from '$lib/components/LogConsole.svelte';
|
||||||
|
|
||||||
// Backup-only progress window: shown for the Play-on-a-config path, which has no
|
// Backup-only progress window: shown for the Play-on-a-config path, which has no
|
||||||
@@ -15,9 +16,14 @@
|
|||||||
logs: string[];
|
logs: string[];
|
||||||
error?: string;
|
error?: string;
|
||||||
onStop?: () => void;
|
onStop?: () => void;
|
||||||
|
// Optional title icon (e.g. the repo-type icon for a destination action).
|
||||||
|
// Defaults to the generic Archive icon when omitted.
|
||||||
|
icon?: Component;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open = $bindable(), title, status, progress = 0, logs, error, onStop }: Props = $props();
|
let { open = $bindable(), title, status, progress = 0, logs, error, onStop, icon }: Props = $props();
|
||||||
|
|
||||||
|
const TitleIcon = $derived(icon ?? Archive);
|
||||||
|
|
||||||
let copied = $state(false);
|
let copied = $state(false);
|
||||||
|
|
||||||
@@ -33,7 +39,7 @@
|
|||||||
<Dialog.Content class="max-w-4xl h-[60vh] flex flex-col overflow-hidden" onOpenAutoFocus={(e) => e.preventDefault()}>
|
<Dialog.Content class="max-w-4xl h-[60vh] flex flex-col overflow-hidden" onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title class="flex items-center gap-2">
|
<Dialog.Title class="flex items-center gap-2">
|
||||||
<Archive class="w-5 h-5" />
|
<TitleIcon class="w-5 h-5 shrink-0" />
|
||||||
{title}
|
{title}
|
||||||
{#if status === 'running'}
|
{#if status === 'running'}
|
||||||
<Badge variant="secondary" class="text-xs gap-1"><Loader2 class="w-3 h-3 animate-spin" />Running</Badge>
|
<Badge variant="secondary" class="text-xs gap-1"><Loader2 class="w-3 h-3 animate-spin" />Running</Badge>
|
||||||
|
|||||||
Reference in new issue
Block a user