Compare commits

...

8 Commits

Author SHA1 Message Date
Nicolas Meienberger
70e4c782ff fix: undefined path in local repo 2025-11-17 21:09:46 +01:00
Nicolas Meienberger
c726c6fc72 feat: custom local repository path 2025-11-17 18:17:51 +01:00
Nicolas Meienberger
4d48d7be58 feat: add support for REST server 2025-11-16 18:24:09 +01:00
Nicolas Meienberger
df6b70c96f fix(create-volume): all port fields as number 2025-11-16 17:27:49 +01:00
Nicolas Meienberger
94423bd0a5 chore: remove unnecessary deps 2025-11-16 17:20:46 +01:00
Nicolas Meienberger
ed2a625fa7 ci: fix app version build arg 2025-11-16 17:11:30 +01:00
Nicolas Meienberger
a3e027694a ci: fix version injection to be a docker build arg 2025-11-16 16:53:29 +01:00
Copilot
0d36484c04 Add "Ironmount" prefix to page titles and display version in sidebar (#28)
* Initial plan

* Initial exploration - understanding the codebase

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

* Add "Ironmount - " prefix to all route titles and version in sidebar

Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: nicotsx <47644445+nicotsx@users.noreply.github.com>
2025-11-16 16:49:35 +01:00
27 changed files with 304 additions and 35 deletions

View File

@@ -74,6 +74,8 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
APP_VERSION=${{ needs.determine-release-type.outputs.tagname }}
publish-release:
runs-on: ubuntu-latest

View File

@@ -59,6 +59,8 @@ CMD ["bun", "run", "dev"]
# ------------------------------
FROM oven/bun:${BUN_VERSION} AS builder
ARG APP_VERSION=dev
WORKDIR /app
COPY ./package.json ./bun.lock ./
@@ -66,6 +68,9 @@ RUN bun install --frozen-lockfile
COPY . .
RUN touch .env
RUN echo "VITE_APP_VERSION=${APP_VERSION}" >> .env
RUN bun run build
FROM base AS production

View File

@@ -741,12 +741,21 @@ export type ListRepositoriesResponses = {
name: string;
customPassword?: string;
isExistingRepository?: boolean;
path?: string;
} | {
backend: 'rclone';
path: string;
remote: string;
customPassword?: string;
isExistingRepository?: boolean;
} | {
backend: 'rest';
url: string;
customPassword?: string;
isExistingRepository?: boolean;
password?: string;
path?: string;
username?: string;
};
createdAt: number;
id: string;
@@ -754,7 +763,7 @@ export type ListRepositoriesResponses = {
lastError: string | null;
name: string;
status: 'error' | 'healthy' | 'unknown' | null;
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
updatedAt: number;
}>;
};
@@ -799,12 +808,21 @@ export type CreateRepositoryData = {
name: string;
customPassword?: string;
isExistingRepository?: boolean;
path?: string;
} | {
backend: 'rclone';
path: string;
remote: string;
customPassword?: string;
isExistingRepository?: boolean;
} | {
backend: 'rest';
url: string;
customPassword?: string;
isExistingRepository?: boolean;
password?: string;
path?: string;
username?: string;
};
name: string;
compressionMode?: 'auto' | 'better' | 'fastest' | 'max' | 'off';
@@ -919,12 +937,21 @@ export type GetRepositoryResponses = {
name: string;
customPassword?: string;
isExistingRepository?: boolean;
path?: string;
} | {
backend: 'rclone';
path: string;
remote: string;
customPassword?: string;
isExistingRepository?: boolean;
} | {
backend: 'rest';
url: string;
customPassword?: string;
isExistingRepository?: boolean;
password?: string;
path?: string;
username?: string;
};
createdAt: number;
id: string;
@@ -932,7 +959,7 @@ export type GetRepositoryResponses = {
lastError: string | null;
name: string;
status: 'error' | 'healthy' | 'unknown' | null;
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
updatedAt: number;
};
};
@@ -1166,12 +1193,21 @@ export type ListBackupSchedulesResponses = {
name: string;
customPassword?: string;
isExistingRepository?: boolean;
path?: string;
} | {
backend: 'rclone';
path: string;
remote: string;
customPassword?: string;
isExistingRepository?: boolean;
} | {
backend: 'rest';
url: string;
customPassword?: string;
isExistingRepository?: boolean;
password?: string;
path?: string;
username?: string;
};
createdAt: number;
id: string;
@@ -1179,7 +1215,7 @@ export type ListBackupSchedulesResponses = {
lastError: string | null;
name: string;
status: 'error' | 'healthy' | 'unknown' | null;
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
updatedAt: number;
};
repositoryId: string;
@@ -1379,12 +1415,21 @@ export type GetBackupScheduleResponses = {
name: string;
customPassword?: string;
isExistingRepository?: boolean;
path?: string;
} | {
backend: 'rclone';
path: string;
remote: string;
customPassword?: string;
isExistingRepository?: boolean;
} | {
backend: 'rest';
url: string;
customPassword?: string;
isExistingRepository?: boolean;
password?: string;
path?: string;
username?: string;
};
createdAt: number;
id: string;
@@ -1392,7 +1437,7 @@ export type GetBackupScheduleResponses = {
lastError: string | null;
name: string;
status: 'error' | 'healthy' | 'unknown' | null;
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
updatedAt: number;
};
repositoryId: string;
@@ -1573,12 +1618,21 @@ export type GetBackupScheduleForVolumeResponses = {
name: string;
customPassword?: string;
isExistingRepository?: boolean;
path?: string;
} | {
backend: 'rclone';
path: string;
remote: string;
customPassword?: string;
isExistingRepository?: boolean;
} | {
backend: 'rest';
url: string;
customPassword?: string;
isExistingRepository?: boolean;
password?: string;
path?: string;
username?: string;
};
createdAt: number;
id: string;
@@ -1586,7 +1640,7 @@ export type GetBackupScheduleForVolumeResponses = {
lastError: string | null;
name: string;
status: 'error' | 'healthy' | 'unknown' | null;
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 's3';
type: 'azure' | 'gcs' | 'local' | 'r2' | 'rclone' | 'rest' | 's3';
updatedAt: number;
};
repositoryId: string;

View File

@@ -3,6 +3,7 @@ import { Link, NavLink } from "react-router";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarHeader,
@@ -13,6 +14,7 @@ import {
} from "~/client/components/ui/sidebar";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "~/client/components/ui/tooltip";
import { cn } from "~/client/lib/utils";
import { APP_VERSION } from "~/client/lib/version";
const items = [
{
@@ -85,6 +87,15 @@ export function AppSidebar() {
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="p-4 border-r border-t border-border/50">
<div
className={cn("text-xs text-muted-foreground transition-all duration-200", {
"opacity-0 w-0 overflow-hidden": state === "collapsed",
})}
>
{APP_VERSION}
</div>
</SidebarFooter>
</Sidebar>
);
}

View File

@@ -10,12 +10,23 @@ import { Input } from "./ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { useQuery } from "@tanstack/react-query";
import { Alert, AlertDescription } from "./ui/alert";
import { ExternalLink } from "lucide-react";
import { ExternalLink, AlertTriangle } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
import { useSystemInfo } from "~/client/hooks/use-system-info";
import { COMPRESSION_MODES, repositoryConfigSchema } from "~/schemas/restic";
import { listRcloneRemotesOptions } from "../api-client/@tanstack/react-query.gen";
import { Checkbox } from "./ui/checkbox";
import { DirectoryBrowser } from "./directory-browser";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
export const formSchema = type({
name: "2<=string<=32",
@@ -41,6 +52,7 @@ const defaultValuesForType = {
gcs: { backend: "gcs" as const, compressionMode: "auto" as const },
azure: { backend: "azure" as const, compressionMode: "auto" as const },
rclone: { backend: "rclone" as const, compressionMode: "auto" as const },
rest: { backend: "rest" as const, compressionMode: "auto" as const },
};
export const CreateRepositoryForm = ({
@@ -66,6 +78,8 @@ export const CreateRepositoryForm = ({
const watchedIsExistingRepository = watch("isExistingRepository");
const [passwordMode, setPasswordMode] = useState<"default" | "custom">("default");
const [showPathBrowser, setShowPathBrowser] = useState(false);
const [showPathWarning, setShowPathWarning] = useState(false);
const { capabilities } = useSystemInfo();
@@ -126,6 +140,7 @@ export const CreateRepositoryForm = ({
<SelectItem value="r2">Cloudflare R2</SelectItem>
<SelectItem value="gcs">Google Cloud Storage</SelectItem>
<SelectItem value="azure">Azure Blob Storage</SelectItem>
<SelectItem value="rest">REST Server</SelectItem>
<Tooltip>
<TooltipTrigger>
<SelectItem disabled={!capabilities.rclone} value="rclone">
@@ -245,6 +260,87 @@ export const CreateRepositoryForm = ({
</>
)}
{watchedBackend === "local" && (
<>
<FormItem>
<FormLabel>Repository Directory</FormLabel>
<div className="flex items-center gap-2">
<div className="flex-1 text-sm font-mono bg-muted px-3 py-2 rounded-md border">
{form.watch("path") || "/var/lib/ironmount/repositories"}
</div>
<Button
type="button"
variant="outline"
onClick={() => setShowPathWarning(true)}
size="sm"
>
Change
</Button>
</div>
<FormDescription>
The directory where the repository will be stored.
</FormDescription>
</FormItem>
<AlertDialog open={showPathWarning} onOpenChange={setShowPathWarning}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<AlertTriangle className="h-5 w-5 text-yellow-500" />
Important: Host Mount Required
</AlertDialogTitle>
<AlertDialogDescription className="space-y-3">
<p>
When selecting a custom path, ensure it is mounted from the host machine into the
container.
</p>
<p className="font-medium">
If the path is not a host mount, you will lose your repository data when the container
restarts.
</p>
<p className="text-sm text-muted-foreground">
The default path <code className="bg-muted px-1 rounded">/var/lib/ironmount/repositories</code> is
already mounted from the host and is safe to use.
</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setShowPathBrowser(true);
setShowPathWarning(false);
}}
>
I Understand, Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={showPathBrowser} onOpenChange={setShowPathBrowser}>
<AlertDialogContent className="max-w-2xl">
<AlertDialogHeader>
<AlertDialogTitle>Select Repository Directory</AlertDialogTitle>
<AlertDialogDescription>
Choose a directory from the filesystem to store the repository.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="py-4">
<DirectoryBrowser
onSelectPath={(path) => form.setValue("path", path)}
selectedPath={form.watch("path") || "/var/lib/ironmount/repositories"}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => setShowPathBrowser(false)}>Done</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)}
{watchedBackend === "s3" && (
<>
<FormField
@@ -546,6 +642,67 @@ export const CreateRepositoryForm = ({
</>
))}
{watchedBackend === "rest" && (
<>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>REST Server URL</FormLabel>
<FormControl>
<Input placeholder="http://192.168.1.30:8000" {...field} />
</FormControl>
<FormDescription>URL of the REST server.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => (
<FormItem>
<FormLabel>Repository Path (Optional)</FormLabel>
<FormControl>
<Input placeholder="my-backup-repo" {...field} />
</FormControl>
<FormDescription>Path to the repository on the REST server (leave empty for root).</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username (Optional)</FormLabel>
<FormControl>
<Input placeholder="username" {...field} />
</FormControl>
<FormDescription>Username for REST server authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password (Optional)</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>Password for REST server authentication.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
{mode === "update" && (
<Button type="submit" className="w-full" loading={loading}>
Save Changes

View File

@@ -207,7 +207,12 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input type="number" placeholder="2049" {...field} />
<Input
type="number"
placeholder="2049"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
/>
</FormControl>
<FormDescription>NFS server port (default: 2049).</FormDescription>
<FormMessage />
@@ -332,7 +337,12 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<Input type="number" placeholder="80" {...field} />
<Input
type="number"
placeholder="80"
{...field}
onChange={(e) => field.onChange(parseInt(e.target.value, 10) || undefined)}
/>
</FormControl>
<FormDescription>WebDAV server port (default: 80 for HTTP, 443 for HTTPS).</FormDescription>
<FormMessage />

View File

@@ -1,4 +1,4 @@
import { Database, HardDrive, Cloud } from "lucide-react";
import { Database, HardDrive, Cloud, Server } from "lucide-react";
import type { RepositoryBackend } from "~/schemas/restic";
type Props = {
@@ -14,6 +14,8 @@ export const RepositoryIcon = ({ backend, className = "h-4 w-4" }: Props) => {
return <Cloud className={className} />;
case "gcs":
return <Cloud className={className} />;
case "rest":
return <Server className={className} />;
default:
return <Database className={className} />;
}

View File

@@ -0,0 +1 @@
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || "dev";

View File

@@ -16,7 +16,7 @@ export const clientMiddleware = [authMiddleware];
export function meta(_: Route.MetaArgs) {
return [
{ title: "Download Recovery Key" },
{ title: "Ironmount - Download Recovery Key" },
{
name: "description",
content: "Download your backup recovery key to ensure you can restore your data.",

View File

@@ -16,7 +16,7 @@ export const clientMiddleware = [authMiddleware];
export function meta(_: Route.MetaArgs) {
return [
{ title: "Login" },
{ title: "Ironmount - Login" },
{
name: "description",
content: "Sign in to your Ironmount account.",

View File

@@ -24,7 +24,7 @@ export const clientMiddleware = [authMiddleware];
export function meta(_: Route.MetaArgs) {
return [
{ title: "Onboarding" },
{ title: "Ironmount - Onboarding" },
{
name: "description",
content: "Welcome to Ironmount. Create your admin account to get started.",

View File

@@ -40,7 +40,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Backup Job Details" },
{ title: "Ironmount - Backup Job Details" },
{
name: "description",
content: "View and manage backup job configuration, schedule, and snapshots.",

View File

@@ -15,7 +15,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Backup Jobs" },
{ title: "Ironmount - Backup Jobs" },
{
name: "description",
content: "Automate volume backups with scheduled jobs and retention policies.",

View File

@@ -24,7 +24,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Create Backup Job" },
{ title: "Ironmount - Create Backup Job" },
{
name: "description",
content: "Create a new automated backup job for your volumes.",

View File

@@ -17,7 +17,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Create Repository" },
{ title: "Ironmount - Create Repository" },
{
name: "description",
content: "Create a new backup repository with encryption and compression.",

View File

@@ -20,7 +20,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Repositories" },
{ title: "Ironmount - Repositories" },
{
name: "description",
content: "Manage your backup repositories with encryption and compression.",

View File

@@ -36,7 +36,7 @@ export const handle = {
export function meta({ params }: Route.MetaArgs) {
return [
{ title: params.name },
{ title: `Ironmount - ${params.name}` },
{
name: "description",
content: "View repository configuration, status, and snapshots.",

View File

@@ -17,7 +17,7 @@ export const handle = {
export function meta({ params }: Route.MetaArgs) {
return [
{ title: `Snapshot ${params.snapshotId}` },
{ title: `Ironmount - Snapshot ${params.snapshotId}` },
{
name: "description",
content: "Browse and restore files from a backup snapshot.",

View File

@@ -30,7 +30,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Settings" },
{ title: "Ironmount - Settings" },
{
name: "description",
content: "Manage your account settings and preferences.",

View File

@@ -17,7 +17,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Create Volume" },
{ title: "Ironmount - Create Volume" },
{
name: "description",
content: "Create a new storage volume with automatic mounting and health checks.",

View File

@@ -37,7 +37,7 @@ export const handle = {
export function meta({ params }: Route.MetaArgs) {
return [
{ title: params.name },
{ title: `Ironmount - ${params.name}` },
{
name: "description",
content: "View and manage volume details, configuration, and files.",

View File

@@ -20,7 +20,7 @@ export const handle = {
export function meta(_: Route.MetaArgs) {
return [
{ title: "Volumes" },
{ title: "Ironmount - Volumes" },
{
name: "description",
content: "Create, manage, monitor, and automate your Docker volumes with ease.",

View File

@@ -7,6 +7,7 @@ export const REPOSITORY_BACKENDS = {
gcs: "gcs",
azure: "azure",
rclone: "rclone",
rest: "rest",
} as const;
export type RepositoryBackend = keyof typeof REPOSITORY_BACKENDS;
@@ -36,6 +37,7 @@ export const r2RepositoryConfigSchema = type({
export const localRepositoryConfigSchema = type({
backend: "'local'",
name: "string",
path: "string?",
}).and(baseRepositoryConfigSchema);
export const gcsRepositoryConfigSchema = type({
@@ -59,12 +61,21 @@ export const rcloneRepositoryConfigSchema = type({
path: "string",
}).and(baseRepositoryConfigSchema);
export const restRepositoryConfigSchema = type({
backend: "'rest'",
url: "string",
username: "string?",
password: "string?",
path: "string?",
}).and(baseRepositoryConfigSchema);
export const repositoryConfigSchema = s3RepositoryConfigSchema
.or(r2RepositoryConfigSchema)
.or(localRepositoryConfigSchema)
.or(gcsRepositoryConfigSchema)
.or(azureRepositoryConfigSchema)
.or(rcloneRepositoryConfigSchema);
.or(rcloneRepositoryConfigSchema)
.or(restRepositoryConfigSchema);
export type RepositoryConfig = typeof repositoryConfigSchema.infer;

View File

@@ -33,6 +33,14 @@ const encryptConfig = async (config: RepositoryConfig): Promise<RepositoryConfig
case "azure":
encryptedConfig.accountKey = await cryptoUtils.encrypt(config.accountKey);
break;
case "rest":
if (config.username) {
encryptedConfig.username = await cryptoUtils.encrypt(config.username);
}
if (config.password) {
encryptedConfig.password = await cryptoUtils.encrypt(config.password);
}
break;
}
return encryptedConfig as RepositoryConfig;

View File

@@ -71,7 +71,7 @@ const ensurePassfile = async () => {
const buildRepoUrl = (config: RepositoryConfig): string => {
switch (config.backend) {
case "local":
return `${REPOSITORY_BASE}/${config.name}`;
return config.path ? `${config.path}/${config.name}` : `${REPOSITORY_BASE}/${config.name}`;
case "s3":
return `s3:${config.endpoint}/${config.bucket}`;
case "r2": {
@@ -84,6 +84,10 @@ const buildRepoUrl = (config: RepositoryConfig): string => {
return `azure:${config.container}:/`;
case "rclone":
return `rclone:${config.remote}:${config.path}`;
case "rest": {
const path = config.path ? `/${config.path}` : "";
return `rest:${config.url}${path}`;
}
default: {
throw new Error(`Unsupported repository backend: ${JSON.stringify(config)}`);
}
@@ -133,6 +137,15 @@ const buildEnv = async (config: RepositoryConfig) => {
}
break;
}
case "rest": {
if (config.username) {
env.RESTIC_REST_USERNAME = await cryptoUtils.decrypt(config.username);
}
if (config.password) {
env.RESTIC_REST_PASSWORD = await cryptoUtils.decrypt(config.password);
}
break;
}
}
return env;
@@ -142,6 +155,9 @@ const init = async (config: RepositoryConfig) => {
await ensurePassfile();
const repoUrl = buildRepoUrl(config);
logger.info(`Initializing restic repository at ${repoUrl}...`);
const env = await buildEnv(config);
const res = await $`restic init --repo ${repoUrl} --json`.env(env).nothrow();

View File

@@ -4,7 +4,6 @@
"": {
"name": "@ironmount/client",
"dependencies": {
"@hono/node-server": "^1.19.6",
"@hono/standard-validator": "^0.1.5",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -23,8 +22,6 @@
"@react-router/serve": "^7.9.3",
"@scalar/hono-api-reference": "^0.9.24",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
"arktype": "^2.1.26",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -60,6 +57,7 @@
"@hey-api/openapi-ts": "^0.87.4",
"@react-router/dev": "^7.9.3",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query-devtools": "^5.90.2",
"@types/bun": "^1.3.2",
"@types/dockerode": "^3.3.45",
"@types/node": "^24.6.2",
@@ -482,10 +480,6 @@
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
"@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="],
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],

View File

@@ -17,7 +17,6 @@
"studio": "drizzle-kit studio"
},
"dependencies": {
"@hono/node-server": "^1.19.6",
"@hono/standard-validator": "^0.1.5",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -36,8 +35,6 @@
"@react-router/serve": "^7.9.3",
"@scalar/hono-api-reference": "^0.9.24",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
"arktype": "^2.1.26",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -73,6 +70,7 @@
"@hey-api/openapi-ts": "^0.87.4",
"@react-router/dev": "^7.9.3",
"@tailwindcss/vite": "^4.1.14",
"@tanstack/react-query-devtools": "^5.90.2",
"@types/bun": "^1.3.2",
"@types/dockerode": "^3.3.45",
"@types/node": "^24.6.2",