Refactor type usage and improve data normalization
Updated several components to use explicit TypeScript types for better type safety. Normalized appriseUrls to always be an array in auto-sync settings API. Improved handling of optional server_id in BackupsTab and adjusted IP detection logic in InstalledScriptsTab. Removed unnecessary eslint-disable comments and improved code clarity in various places.
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import eslintPluginNext from "@next/eslint-plugin-next";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: import.meta.dirname,
|
||||
});
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [".next", "next-env.d.ts", "postcss.config.js", "prettier.config.js"],
|
||||
},
|
||||
...compat.extends("next/core-web-vitals"),
|
||||
{
|
||||
plugins: {
|
||||
"@next/next": eslintPluginNext,
|
||||
"react": reactPlugin,
|
||||
"react-hooks": reactHooksPlugin,
|
||||
},
|
||||
rules: {
|
||||
...eslintPluginNext.configs.recommended.rules,
|
||||
...eslintPluginNext.configs["core-web-vitals"].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
extends: [
|
||||
|
||||
@@ -97,7 +97,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
|
||||
|
||||
const checkAuth = useCallback(() => {
|
||||
return checkAuthInternal(0);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
}, []);
|
||||
|
||||
const login = async (
|
||||
|
||||
@@ -32,7 +32,7 @@ interface Backup {
|
||||
storage_name: string;
|
||||
storage_type: string;
|
||||
discovered_at: Date;
|
||||
server_id: number;
|
||||
server_id?: number;
|
||||
server_name: string | null;
|
||||
server_color: string | null;
|
||||
}
|
||||
@@ -163,7 +163,6 @@ export function BackupsTab() {
|
||||
}
|
||||
setHasAutoDiscovered(true);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hasAutoDiscovered, isLoading, backupsData]);
|
||||
|
||||
const handleDiscoverBackups = () => {
|
||||
@@ -188,7 +187,7 @@ export function BackupsTab() {
|
||||
restoreMutation.mutate({
|
||||
backupId: selectedBackup.backup.id,
|
||||
containerId: selectedBackup.containerId,
|
||||
serverId: selectedBackup.backup.server_id,
|
||||
serverId: selectedBackup.backup.server_id ?? 0,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -321,7 +321,7 @@ export function FilterBar({
|
||||
|
||||
{/* Repository Filter Buttons - Only show if more than one enabled repo */}
|
||||
{enabledRepos.length > 1 &&
|
||||
enabledRepos.map((repo) => {
|
||||
enabledRepos.map((repo: { id: number; url: string }) => {
|
||||
const repoUrl = String(repo.url);
|
||||
const isSelected =
|
||||
filters.selectedRepositories.includes(repoUrl);
|
||||
|
||||
@@ -1704,134 +1704,143 @@ export function GeneralSettingsModal({
|
||||
{repositoriesData?.success &&
|
||||
repositoriesData.repositories.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{repositoriesData.repositories.map((repo) => (
|
||||
<div
|
||||
key={repo.id}
|
||||
className="border-border flex items-center justify-between gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<a
|
||||
href={repo.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground hover:text-primary flex items-center gap-1 text-sm font-medium"
|
||||
>
|
||||
{repo.url}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
{repo.is_default && (
|
||||
<span className="bg-primary/10 text-primary rounded px-2 py-0.5 text-xs">
|
||||
{repo.priority === 1 ? "Main" : "Dev"}
|
||||
</span>
|
||||
)}
|
||||
{repositoriesData.repositories.map(
|
||||
(repo: {
|
||||
id: number;
|
||||
url: string;
|
||||
enabled: boolean;
|
||||
is_default: boolean;
|
||||
is_removable: boolean;
|
||||
priority: number;
|
||||
}) => (
|
||||
<div
|
||||
key={repo.id}
|
||||
className="border-border flex items-center justify-between gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<a
|
||||
href={repo.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground hover:text-primary flex items-center gap-1 text-sm font-medium"
|
||||
>
|
||||
{repo.url}
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
{repo.is_default && (
|
||||
<span className="bg-primary/10 text-primary rounded px-2 py-0.5 text-xs">
|
||||
{repo.priority === 1 ? "Main" : "Dev"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Priority: {repo.priority}{" "}
|
||||
{repo.enabled ? "• Enabled" : "• Disabled"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Priority: {repo.priority}{" "}
|
||||
{repo.enabled ? "• Enabled" : "• Disabled"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Toggle
|
||||
checked={repo.enabled}
|
||||
onCheckedChange={async (enabled) => {
|
||||
setMessage(null);
|
||||
try {
|
||||
const result =
|
||||
await updateRepoMutation.mutateAsync({
|
||||
id: repo.id,
|
||||
enabled,
|
||||
});
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: `Repository ${enabled ? "enabled" : "disabled"} successfully!`,
|
||||
});
|
||||
await refetchRepositories();
|
||||
} else {
|
||||
<div className="flex items-center gap-2">
|
||||
<Toggle
|
||||
checked={repo.enabled}
|
||||
onCheckedChange={async (enabled) => {
|
||||
setMessage(null);
|
||||
try {
|
||||
const result =
|
||||
await updateRepoMutation.mutateAsync({
|
||||
id: repo.id,
|
||||
enabled,
|
||||
});
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: `Repository ${enabled ? "enabled" : "disabled"} successfully!`,
|
||||
});
|
||||
await refetchRepositories();
|
||||
} else {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
result.error ??
|
||||
"Failed to update repository",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
result.error ??
|
||||
"Failed to update repository",
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to update repository",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to update repository",
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={updateRepoMutation.isPending}
|
||||
label={repo.enabled ? "Disable" : "Enable"}
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!repo.is_removable) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: "Default repositories cannot be deleted",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to delete this repository? All scripts from this repository will be removed.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDeletingRepoId(Number(repo.id));
|
||||
setMessage(null);
|
||||
try {
|
||||
const result =
|
||||
await deleteRepoMutation.mutateAsync({
|
||||
id: repo.id,
|
||||
});
|
||||
if (result.success) {
|
||||
}}
|
||||
disabled={updateRepoMutation.isPending}
|
||||
label={repo.enabled ? "Disable" : "Enable"}
|
||||
/>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!repo.is_removable) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "Repository deleted successfully!",
|
||||
type: "error",
|
||||
text: "Default repositories cannot be deleted",
|
||||
});
|
||||
await refetchRepositories();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure you want to delete this repository? All scripts from this repository will be removed.`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDeletingRepoId(Number(repo.id));
|
||||
setMessage(null);
|
||||
try {
|
||||
const result =
|
||||
await deleteRepoMutation.mutateAsync({
|
||||
id: repo.id,
|
||||
});
|
||||
if (result.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: "Repository deleted successfully!",
|
||||
});
|
||||
await refetchRepositories();
|
||||
} else {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
result.error ??
|
||||
"Failed to delete repository",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
result.error ??
|
||||
"Failed to delete repository",
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to delete repository",
|
||||
});
|
||||
} finally {
|
||||
setDeletingRepoId(null);
|
||||
}
|
||||
} catch (error) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Failed to delete repository",
|
||||
});
|
||||
} finally {
|
||||
setDeletingRepoId(null);
|
||||
}}
|
||||
disabled={
|
||||
!repo.is_removable ||
|
||||
deletingRepoId === repo.id ||
|
||||
deleteRepoMutation.isPending
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
!repo.is_removable ||
|
||||
deletingRepoId === repo.id ||
|
||||
deleteRepoMutation.isPending
|
||||
}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-error hover:text-error/80 hover:bg-error/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-error hover:text-error/80 hover:bg-error/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
|
||||
@@ -331,7 +331,7 @@ export function InstalledScriptsTab() {
|
||||
setAutoDetectStatus({
|
||||
type: "success",
|
||||
message: data.success
|
||||
? `Detected IP: ${data.ip}`
|
||||
? `Detected IP: ${data.detectedIp ?? "unknown"}`
|
||||
: (data.error ?? "Failed to detect Web UI"),
|
||||
});
|
||||
setTimeout(
|
||||
@@ -359,15 +359,10 @@ export function InstalledScriptsTab() {
|
||||
{ enabled: false }, // Only fetch when explicitly called
|
||||
);
|
||||
|
||||
const fetchStorages = async (serverId: number, forceRefresh = false) => {
|
||||
const fetchStorages = async (serverId: number, _forceRefresh = false) => {
|
||||
setIsLoadingStorages(true);
|
||||
try {
|
||||
const result = await getBackupStoragesQuery.refetch({
|
||||
queryKey: [
|
||||
"installedScripts.getBackupStorages",
|
||||
{ serverId, forceRefresh },
|
||||
],
|
||||
});
|
||||
const result = await getBackupStoragesQuery.refetch();
|
||||
if (result.data?.success) {
|
||||
setBackupStorages(result.data.storages);
|
||||
} else {
|
||||
|
||||
@@ -74,7 +74,7 @@ export function ServerForm({
|
||||
// Check for IPv6 with zone identifier (link-local addresses like fe80::...%eth0)
|
||||
let ipv6Address = trimmed;
|
||||
const zoneIdMatch = /^(.+)%([a-zA-Z0-9_\-]+)$/.exec(trimmed);
|
||||
if (zoneIdMatch) {
|
||||
if (zoneIdMatch?.[1] && zoneIdMatch[2]) {
|
||||
ipv6Address = zoneIdMatch[1];
|
||||
// Zone identifier should be a valid interface name (alphanumeric, underscore, hyphen)
|
||||
const zoneId = zoneIdMatch[2];
|
||||
|
||||
@@ -380,7 +380,7 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [scriptPath, mode, server, isUpdate, isShell, containerId, isMobile]);
|
||||
|
||||
const startScript = () => {
|
||||
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN && !isRunning) {
|
||||
|
||||
@@ -187,7 +187,16 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Update the global service instance with new settings
|
||||
autoSyncService.saveSettings(settings);
|
||||
// Normalize appriseUrls to always be an array
|
||||
const normalizedSettings = {
|
||||
...settings,
|
||||
appriseUrls: Array.isArray(settings.appriseUrls)
|
||||
? settings.appriseUrls
|
||||
: settings.appriseUrls
|
||||
? [settings.appriseUrls]
|
||||
: undefined
|
||||
};
|
||||
autoSyncService.saveSettings(normalizedSettings);
|
||||
|
||||
if (settings.autoSyncEnabled) {
|
||||
autoSyncService.scheduleAutoSync();
|
||||
|
||||
@@ -476,7 +476,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const scripts = await db.getAllInstalledScripts();
|
||||
|
||||
// Transform scripts to flatten server data for frontend compatibility
|
||||
const transformedScripts = await Promise.all(scripts.map(async (script) => {
|
||||
|
||||
const transformedScripts = await Promise.all(scripts.map(async (script: any) => {
|
||||
// Determine if it's a VM or LXC
|
||||
let is_vm = false;
|
||||
if (script.container_id && script.server_id) {
|
||||
@@ -522,7 +523,8 @@ export const installedScriptsRouter = createTRPCRouter({
|
||||
const scripts = await db.getInstalledScriptsByServer(input.serverId);
|
||||
|
||||
// Transform scripts to flatten server data for frontend compatibility
|
||||
const transformedScripts = await Promise.all(scripts.map(async (script) => {
|
||||
|
||||
const transformedScripts = await Promise.all(scripts.map(async (script: any) => {
|
||||
// Determine if it's a VM or LXC
|
||||
let is_vm = false;
|
||||
if (script.container_id && script.server_id) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||
import { scriptManager } from "~/server/lib/scripts";
|
||||
|
||||
Reference in New Issue
Block a user