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:
CanbiZ
2025-11-28 12:47:09 +01:00
parent 7547dff67d
commit 03e31d66a7
11 changed files with 160 additions and 138 deletions

View File

@@ -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: [

View File

@@ -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 (

View File

@@ -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,
});
};

View File

@@ -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);

View File

@@ -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">

View File

@@ -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 {

View File

@@ -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];

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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";