fix(lint): resolve remaining ESLint errors (unused vars, any types, react-refresh)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-14 22:50:50 -05:00
parent 8cf5f12ffe
commit fb3669f9ac
54 changed files with 144 additions and 155 deletions

View File

@@ -22,7 +22,7 @@ import {
DialogDescription,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { performRegistration, assertWebCryptoAvailable } from '@/lib/crypto/registration'
import { performRegistration } from '@/lib/crypto/registration'
import { keyStore } from '@/lib/crypto/keyStore'
import { authApi } from '@/lib/api'
import { EmergencyKitDialog } from './EmergencyKitDialog'

View File

@@ -56,7 +56,7 @@ export function BulkDeployDialog({
queryKey: ['devices-for-cert', tenantId],
queryFn: async () => {
const result = await devicesApi.list(tenantId)
return (result as any).items ?? result
return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[])
},
enabled: !!tenantId && open,
})
@@ -128,14 +128,15 @@ export function BulkDeployDialog({
variant: 'destructive',
})
}
} catch (e: any) {
} catch (e: unknown) {
const err = e as { response?: { data?: { detail?: string } } }
setResult({
success: 0,
failed: selected.size,
errors: [
{
device_id: 'bulk',
error: e?.response?.data?.detail || 'Bulk deployment failed',
error: err?.response?.data?.detail || 'Bulk deployment failed',
},
],
})

View File

@@ -36,11 +36,13 @@ export function CAStatusCard({ ca, canWrite: writable, tenantId }: CAStatusCardP
void queryClient.invalidateQueries({ queryKey: ['ca'] })
toast({ title: 'Certificate Authority initialized' })
},
onError: (e: any) =>
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({
title: e?.response?.data?.detail || 'Failed to initialize CA',
title: err?.response?.data?.detail || 'Failed to initialize CA',
variant: 'destructive',
}),
})
},
})
const handleDownloadPEM = async () => {

View File

@@ -43,6 +43,7 @@ export function CertConfirmDialog({
// Reset confirm text when dialog opens/closes or action changes
useEffect(() => {
if (open) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setConfirmText('')
}
}, [open, action])

View File

@@ -9,11 +9,7 @@
import { useQuery } from '@tanstack/react-query'
import { useUIStore } from '@/lib/store'
import { Shield, Building2 } from 'lucide-react'
import {
certificatesApi,
type CAResponse,
type DeviceCertResponse,
} from '@/lib/certificatesApi'
import { certificatesApi } from '@/lib/certificatesApi'
import { useAuth, isSuperAdmin } from '@/lib/auth'
import { canWrite } from '@/lib/auth'
import { CAStatusCard } from './CAStatusCard'

View File

@@ -58,7 +58,7 @@ export function DeployCertDialog({
queryFn: async () => {
const result = await devicesApi.list(tenantId)
// The list endpoint returns { items, total, ... } or an array
return (result as any).items ?? result
return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[])
},
enabled: !!tenantId && open,
})
@@ -110,9 +110,10 @@ export function DeployCertDialog({
setErrorMsg(result.error ?? 'Deployment failed')
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
}
} catch (e: any) {
} catch (e: unknown) {
setStep('error')
const detail = e?.response?.data?.detail || 'Failed to deploy certificate'
const err = e as { response?: { data?: { detail?: string } } }
const detail = err?.response?.data?.detail || 'Failed to deploy certificate'
setErrorMsg(detail)
toast({ title: detail, variant: 'destructive' })
}

View File

@@ -7,7 +7,6 @@ import { useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import {
ShieldCheck,
ShieldAlert,
Plus,
Layers,
MoreHorizontal,
@@ -132,11 +131,13 @@ export function DeviceCertTable({
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
}
},
onError: (e: any) =>
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({
title: e?.response?.data?.detail || 'Failed to deploy certificate',
title: err?.response?.data?.detail || 'Failed to deploy certificate',
variant: 'destructive',
}),
})
},
})
const rotateMutation = useMutation({
@@ -149,11 +150,13 @@ export function DeviceCertTable({
toast({ title: result.error ?? 'Rotation failed', variant: 'destructive' })
}
},
onError: (e: any) =>
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({
title: e?.response?.data?.detail || 'Failed to rotate certificate',
title: err?.response?.data?.detail || 'Failed to rotate certificate',
variant: 'destructive',
}),
})
},
})
const revokeMutation = useMutation({
@@ -162,11 +165,13 @@ export function DeviceCertTable({
void queryClient.invalidateQueries({ queryKey: ['deviceCerts'] })
toast({ title: 'Certificate revoked' })
},
onError: (e: any) =>
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({
title: e?.response?.data?.detail || 'Failed to revoke certificate',
title: err?.response?.data?.detail || 'Failed to revoke certificate',
variant: 'destructive',
}),
})
},
})
// ── Filtering ──

View File

@@ -7,10 +7,10 @@
* users can browse RouterOS menu paths and manage entries.
*/
import { useState, useCallback } from 'react'
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Terminal, ChevronRight, Loader2, WifiOff, Building2 } from 'lucide-react'
import { configEditorApi, type BrowseResponse } from '@/lib/configEditorApi'
import { configEditorApi } from '@/lib/configEditorApi'
import { metricsApi } from '@/lib/api'
import { useAuth, isSuperAdmin } from '@/lib/auth'
import { useUIStore } from '@/lib/store'

View File

@@ -3,7 +3,7 @@
* in a dynamic table with edit/delete action buttons.
*/
import { Pencil, Trash2, Plus, Loader2 } from 'lucide-react'
import { Pencil, Trash2, Plus } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { canWrite } from '@/lib/auth'
@@ -19,8 +19,6 @@ interface EntryTableProps {
onAdd: () => void
}
/** Read-only fields that should not have edit buttons */
const READ_ONLY_FIELDS = new Set(['.id', 'running', 'dynamic', 'default', 'invalid'])
export function EntryTable({
entries,

View File

@@ -22,7 +22,6 @@ import {
import { SafetyToggle } from './SafetyToggle'
import { ChangePreviewModal } from './ChangePreviewModal'
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
import { cn } from '@/lib/utils'
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
// ---------------------------------------------------------------------------
@@ -48,8 +47,6 @@ interface AddressListForm {
const EMPTY_FORM: AddressListForm = { list: '', address: '', comment: '' }
type PanelHook = ReturnType<typeof useConfigPanel>
// ---------------------------------------------------------------------------
// AddressListPanel
// ---------------------------------------------------------------------------

View File

@@ -13,7 +13,6 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { configEditorApi } from '@/lib/configEditorApi'
import { cn } from '@/lib/utils'
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
interface BwResult {

View File

@@ -48,12 +48,6 @@ export function BridgeVlanPanel({ tenantId, deviceId, active }: ConfigPanelProps
[ports.entries],
)
// Check if VLAN filtering is enabled on any bridge
const vlanFilteringBridges = useMemo(
() => bridges.entries.filter((b) => b['vlan-filtering'] === 'true' || b['vlan-filtering'] === 'yes'),
[bridges.entries],
)
const handleAdd = useCallback(() => {
setEditEntry(null)
setFormData({

View File

@@ -22,15 +22,12 @@ import {
import { SafetyToggle } from './SafetyToggle'
import { ChangePreviewModal } from './ChangePreviewModal'
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
import { cn } from '@/lib/utils'
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type PanelHook = ReturnType<typeof useConfigPanel>
// Timeout fields we expose for editing
const TIMEOUT_FIELDS = [
{ key: 'tcp-established-timeout', label: 'TCP Established' },

View File

@@ -48,8 +48,8 @@ export function DiffViewer({ tenantId, deviceId, snapshotId, onClose }: DiffView
{/* Content */}
{isLoading ? (
<div className="space-y-2">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${60 + Math.random() * 40}%` }} />
{[75, 90, 65, 85, 70, 80].map((w, i) => (
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${w}%` }} />
))}
</div>
) : isError || !diff ? (

View File

@@ -6,15 +6,13 @@
* provides a visual rule builder form, and supports move up/down reordering.
*/
import { useState, useCallback, useMemo } from 'react'
import { useState, useCallback } from 'react'
import { toast } from 'sonner'
import {
Plus,
MoreHorizontal,
Pencil,
Trash2,
ArrowUp,
ArrowDown,
Eye,
EyeOff,
Shield,
@@ -472,11 +470,6 @@ export function FirewallPanel({ tenantId, deviceId, active }: ConfigPanelProps)
setPreviewOpen(false)
}, [panel])
const afterApply = useMemo(() => {
// When applyChanges succeeds, the hook auto-refetches via queryClient.invalidateQueries
return panel.pendingChanges.length
}, [panel.pendingChanges.length])
// -------------------------------------------------------------------------
// Render
// -------------------------------------------------------------------------

View File

@@ -7,7 +7,7 @@
*/
import { useState, useCallback, useMemo } from 'react'
import { Plus, Pencil, Trash2, ArrowUp, ArrowDown, Filter } from 'lucide-react'
import { Plus, Pencil, Trash2, Filter } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'

View File

@@ -197,7 +197,8 @@ function PoolTable({
entries,
panel,
poolUsedBy,
existingPools,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
existingPools: _existingPools,
}: {
entries: PoolEntry[]
panel: PanelHook

View File

@@ -44,7 +44,7 @@ import { cn } from '@/lib/utils'
import { useConfigBrowse, useConfigPanel } from '@/hooks/useConfigPanel'
import { SafetyToggle } from '@/components/config/SafetyToggle'
import { ChangePreviewModal } from '@/components/config/ChangePreviewModal'
import type { ConfigPanelProps, ConfigChange } from '@/lib/configPanelTypes'
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
// ---------------------------------------------------------------------------
// Types

View File

@@ -6,11 +6,10 @@
*/
import { useState, useCallback, useMemo } from 'react'
import { Plus, Pencil, Trash2, Route, Filter } from 'lucide-react'
import { Plus, Pencil, Trash2, Route } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,

View File

@@ -29,15 +29,6 @@ interface TorchEntry {
rx: string
}
function formatBytes(val: string): string {
const n = parseInt(val, 10)
if (isNaN(n)) return val || '-'
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)} GB`
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} MB`
if (n >= 1_000) return `${(n / 1_000).toFixed(0)} KB`
return `${n} B`
}
function formatBps(val: string): string {
const n = parseInt(val, 10)
if (isNaN(n)) return val || '-'

View File

@@ -618,8 +618,6 @@ function WirelessEditDialog({
}) {
const [showPassphrase, setShowPassphrase] = useState(false)
const ssid = entry?.ssid || entry?.['configuration.ssid'] || ''
const [formData, setFormData] = useState<WirelessFormData>({
ssid: '',
band: '',
@@ -631,7 +629,8 @@ function WirelessEditDialog({
})
// Reset form when entry changes
const entryId = entry?.['.id'] || ''
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _entryId = entry?.['.id'] || ''
useState(() => {
if (entry) {
setFormData({

View File

@@ -51,7 +51,8 @@ export function AlertSummary({
criticalCount,
warningCount,
infoCount,
tenantId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
tenantId: _tenantId,
}: AlertSummaryProps) {
const total = criticalCount + warningCount + infoCount
const counts: Record<string, number> = {

View File

@@ -58,6 +58,7 @@ function getScoreTier(score: number): ScoreTier {
* - Memory healthy % (weight 0.2) -- % of online devices with memory < 80%
* - Critical alert penalty (weight 0.2) -- 100 if 0 critical, 50 if 1-2, 0 if 3+
*/
// eslint-disable-next-line react-refresh/only-export-components
export function computeHealthScore(
devices: HealthScoreProps['devices'],
criticalAlerts: number,
@@ -115,7 +116,6 @@ const CIRCUMFERENCE = 2 * Math.PI * RADIUS
export function HealthScore({
devices,
activeAlerts: _activeAlerts,
criticalAlerts,
}: HealthScoreProps) {
const totalDevices = devices.length

View File

@@ -14,6 +14,7 @@ export interface KpiCardsProps {
* Formats bytes-per-second into a human-readable bandwidth string.
* Auto-scales through bps, Kbps, Mbps, Gbps.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function formatBandwidth(bps: number): { value: number; unit: string } {
if (bps < 1_000) return { value: bps, unit: 'bps' }
if (bps < 1_000_000) return { value: bps / 1_000, unit: 'Kbps' }

View File

@@ -16,7 +16,6 @@ import {
} from 'lucide-react'
import {
firmwareApi,
type FirmwareOverview,
type DeviceFirmwareStatus,
type FirmwareVersionGroup,
} from '@/lib/firmwareApi'
@@ -24,7 +23,6 @@ import { useUIStore } from '@/lib/store'
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,

View File

@@ -17,10 +17,7 @@ import {
XCircle,
PauseCircle,
} from 'lucide-react'
import {
firmwareApi,
type FirmwareUpgradeJob,
} from '@/lib/firmwareApi'
import { firmwareApi } from '@/lib/firmwareApi'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

View File

@@ -8,7 +8,7 @@
* Step 5: Import & Verify (bulk-add, then check connectivity)
*/
import { useState, useCallback, useEffect } from 'react'
import { useState, useCallback } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router'
import {
@@ -31,8 +31,6 @@ import {
type SubnetScanResponse,
type SubnetScanResult,
type DeviceResponse,
type DeviceGroupResponse,
type DeviceTagResponse,
} from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'

View File

@@ -32,7 +32,9 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
(s) => s.status === 'active' || s.status === 'creating',
)
if (active) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSession(active)
// eslint-disable-next-line react-hooks/set-state-in-effect
setState(active.status === 'active' ? 'active' : 'connecting')
}
}
@@ -66,6 +68,7 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
// Countdown timer for session expiry
useEffect(() => {
if (state !== 'active' || !session?.expires_at) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setCountdown(null)
return
}
@@ -96,9 +99,10 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
setState('connecting')
}
},
onError: (err: any) => {
onError: (err: unknown) => {
const e = err as { response?: { data?: { detail?: string } } }
setState('failed')
setError(err.response?.data?.detail || 'Failed to create session')
setError(e.response?.data?.detail || 'Failed to create session')
},
})
@@ -113,9 +117,10 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
setError(null)
queryClient.invalidateQueries({ queryKey: ['remote-winbox-sessions', tenantId, deviceId] })
},
onError: (err: any) => {
onError: (err: unknown) => {
const e = err as { response?: { data?: { detail?: string } } }
setState('failed')
setError(err.response?.data?.detail || 'Failed to close session')
setError(e.response?.data?.detail || 'Failed to close session')
},
})
@@ -130,12 +135,6 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
closeMutation.mutate()
}, [closeMutation])
const handleRetry = useCallback(() => {
setSession(null)
setError(null)
handleOpen()
}, [handleOpen])
const handleReset = useCallback(async () => {
try {
const sessions = await remoteWinboxApi.list(tenantId, deviceId)

View File

@@ -32,9 +32,10 @@ export function WinBoxButton({ tenantId, deviceId }: WinBoxButtonProps) {
window.open(data.winbox_uri, '_blank')
}
},
onError: (err: any) => {
onError: (err: unknown) => {
const e = err as { response?: { data?: { detail?: string } } }
setState('error')
setError(err.response?.data?.detail || 'Failed to open tunnel')
setError(e.response?.data?.detail || 'Failed to open tunnel')
},
})

View File

@@ -60,23 +60,6 @@ export function MaintenanceForm({
const devices = deviceData?.items ?? []
// Populate form when editing
useEffect(() => {
if (editWindow) {
setName(editWindow.name)
// Convert ISO to datetime-local format
setStartAt(toDatetimeLocal(editWindow.start_at))
setEndAt(toDatetimeLocal(editWindow.end_at))
setSuppressAlerts(editWindow.suppress_alerts)
setNotes(editWindow.notes ?? '')
const hasDevices = editWindow.device_ids.length > 0
setAllDevices(!hasDevices)
setSelectedDevices(hasDevices ? editWindow.device_ids : [])
} else {
resetForm()
}
}, [editWindow, open])
function resetForm() {
setName('')
setStartAt('')
@@ -94,6 +77,25 @@ export function MaintenanceForm({
return local.toISOString().slice(0, 16)
}
// Populate form when editing
useEffect(() => {
/* eslint-disable react-hooks/set-state-in-effect */
if (editWindow) {
setName(editWindow.name)
// Convert ISO to datetime-local format
setStartAt(toDatetimeLocal(editWindow.start_at))
setEndAt(toDatetimeLocal(editWindow.end_at))
setSuppressAlerts(editWindow.suppress_alerts)
setNotes(editWindow.notes ?? '')
const hasDevices = editWindow.device_ids.length > 0
setAllDevices(!hasDevices)
setSelectedDevices(hasDevices ? editWindow.device_ids : [])
} else {
resetForm()
}
/* eslint-enable react-hooks/set-state-in-effect */
}, [editWindow, open])
const createMutation = useMutation({
mutationFn: (data: MaintenanceWindowCreate) =>
maintenanceApi.create(tenantId, data),

View File

@@ -22,6 +22,7 @@ function toDatetimeLocal(iso: string): string {
/**
* Returns start/end ISO strings for a given preset range or custom range.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function getTimeRange(
range: string,
customStart?: string,
@@ -64,6 +65,7 @@ export function getTimeRange(
* Returns refetchInterval (ms) for short ranges, false for longer ones.
* Per user decision: 1h and 6h auto-refresh every 60 seconds.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function shouldAutoRefresh(range: string): number | false {
if (range === '1h' || range === '6h') return 60_000
return false

View File

@@ -162,7 +162,7 @@ export function ClientsTab({ tenantId, deviceId, active }: ClientsTabProps) {
}
return sortDir === 'asc' ? cmp : -cmp
})
}, [data?.clients, searchQuery, sortField, sortDir])
}, [data, searchQuery, sortField, sortDir])
// Stats
const totalClients = data?.clients.length ?? 0

View File

@@ -53,7 +53,7 @@ interface GaugeBarProps {
direction: 'RX' | 'TX'
}
function GaugeBar({ label, value, maxSpeed, direction }: GaugeBarProps) {
function GaugeBar({ value, maxSpeed, direction }: GaugeBarProps) {
const pct = Math.min((value / maxSpeed) * 100, 100)
const colorClass = getBarColor(pct)

View File

@@ -149,7 +149,7 @@ interface TooltipData {
y: number
}
function NodeTooltip({ data, onClose }: { data: TooltipData; onClose: () => void }) {
function NodeTooltip({ data }: { data: TooltipData; onClose?: () => void }) {
return (
<div
className="absolute z-50 rounded-lg border border-border bg-elevated shadow-lg px-3 py-2 text-xs pointer-events-none"

View File

@@ -25,7 +25,6 @@ import {
devicesApi,
deviceGroupsApi,
type DeviceResponse,
type DeviceGroupResponse,
} from '@/lib/api'
import { configEditorApi } from '@/lib/configEditorApi'
import { Button } from '@/components/ui/button'

View File

@@ -5,7 +5,7 @@ import { useAuth, isSuperAdmin, isTenantAdmin } from '@/lib/auth'
import { authApi } from '@/lib/api'
import { getSMTPSettings, updateSMTPSettings, testSMTPSettings, clearWinboxSessions } from '@/lib/settingsApi'
import { SMTP_PRESETS } from '@/lib/smtpPresets'
import { Settings, User, Shield, Info, Key, Lock, ChevronRight, Download, Trash2, AlertTriangle, Mail, Monitor } from 'lucide-react'
import { User, Shield, Info, Key, Lock, ChevronRight, Download, Trash2, AlertTriangle, Mail, Monitor } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
@@ -365,8 +365,9 @@ function SMTPSettingsSection() {
if (result.success) {
saveMutation.mutate()
}
} catch (e: any) {
setTestResult({ success: false, message: e.response?.data?.message || e.message })
} catch (e: unknown) {
const err = e as { response?: { data?: { message?: string } }; message?: string }
setTestResult({ success: false, message: err.response?.data?.message || err.message || 'Unknown error' })
} finally {
setTesting(false)
}

View File

@@ -353,6 +353,7 @@ function VerifyConnectivityStep({ tenantId, deviceId, onComplete }: Step3Props)
}, [tenantId, deviceId, cleanup])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
startPolling()
return cleanup
}, [startPolling, cleanup])

View File

@@ -5,7 +5,7 @@
* Simpler than the Standard DnsPanel: no TTL, no MX/TXT types, no advanced settings.
*/
import { useState, useEffect } from 'react'
import { useState } from 'react'
import { Server, Globe, Plus, Pencil, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'

View File

@@ -42,19 +42,26 @@ export function LanDhcpPanel({ tenantId, deviceId, active }: ConfigPanelProps) {
const [leaseTime, setLeaseTime] = useState('')
// Sync from browse data
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (lanEntry) setLanAddress(lanEntry.address ?? '')
}, [lanEntry])
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (poolEntry) setPoolRange(poolEntry.ranges ?? '')
}, [poolEntry])
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => {
if (networkEntry) {
/* eslint-disable react-hooks/set-state-in-effect */
setDhcpGateway(networkEntry.gateway ?? '')
setDhcpDns(networkEntry['dns-server'] ?? '')
setLeaseTime(networkEntry['lease-time'] ?? '')
/* eslint-enable react-hooks/set-state-in-effect */
}
}, [networkEntry])

View File

@@ -35,6 +35,7 @@ export function WifiSimplePanel({ tenantId, deviceId, active, routerosVersion }:
const [formState, setFormState] = useState<Record<string, Record<string, string>>>({})
// Sync form state from browse data
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => {
const newState: Record<string, Record<string, string>> = {}
wireless.entries.forEach((entry) => {
@@ -60,6 +61,7 @@ export function WifiSimplePanel({ tenantId, deviceId, active, routerosVersion }:
}
})
if (Object.keys(newState).length > 0) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setFormState((prev) => ({ ...prev, ...newState }))
}
}, [wireless.entries, isV7])

View File

@@ -31,7 +31,7 @@ interface TemplateEditorProps {
const VARIABLE_TYPES = ['string', 'ip', 'integer', 'boolean', 'subnet'] as const
export function TemplateEditor({ tenantId: _tenantId, template, onSave, onCancel }: TemplateEditorProps) {
export function TemplateEditor({ template, onSave, onCancel }: TemplateEditorProps) {
const [name, setName] = useState(template?.name ?? '')
const [description, setDescription] = useState(template?.description ?? '')
const [content, setContent] = useState(template?.content ?? '')

View File

@@ -89,6 +89,7 @@ export function TemplatePushWizard({ open, onClose, tenantId, template }: Templa
const selectedDevices = devices?.filter((d) => selectedDeviceIds.has(d.id)) ?? []
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleGroupSelect = (_groupId: string) => {
// For now, just select all online devices. In a real implementation,
// we'd load group members from the API. Here we select all devices

View File

@@ -46,4 +46,5 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)
Button.displayName = 'Button'
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants }

View File

@@ -1,6 +1,6 @@
import { cn } from '@/lib/utils'
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
type SkeletonProps = React.HTMLAttributes<HTMLDivElement>
export function Skeleton({ className, ...props }: SkeletonProps) {
return (

View File

@@ -25,6 +25,7 @@ interface ToastOptions {
variant?: 'default' | 'destructive'
}
// eslint-disable-next-line react-refresh/only-export-components
export function toast(options: ToastOptions) {
if (options.variant === 'destructive') {
sonnerToast.error(options.title, {
@@ -45,4 +46,5 @@ export const Toast = () => null
export const ToastTitle = () => null
export const ToastDescription = () => null
export const ToastClose = () => null
// eslint-disable-next-line react-refresh/only-export-components
export const useToasts = () => ({ toasts: [] as never[], dismiss: () => {} })

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react'
import { useMutation, useQuery } from '@tanstack/react-query'
import { CheckCircle2, Copy, Loader2, AlertTriangle, Wifi } from 'lucide-react'
import { CheckCircle2, Copy, AlertTriangle, Wifi } from 'lucide-react'
import { vpnApi } from '@/lib/api'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -61,6 +61,7 @@ export function VpnOnboardingWizard({ tenantId, onSuccess, onCancel }: Props) {
// Timer for waiting step
useEffect(() => {
if (step !== 'waiting') return
// eslint-disable-next-line react-hooks/set-state-in-effect
setElapsed(0)
const interval = setInterval(() => setElapsed((e) => e + 1), 1000)
return () => clearInterval(interval)

View File

@@ -26,15 +26,10 @@ import {
import {
vpnApi,
devicesApi,
type VpnConfigResponse,
type VpnPeerResponse,
type VpnPeerConfig,
type DeviceResponse,
} from '@/lib/api'
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Dialog,
DialogContent,
@@ -63,7 +58,6 @@ export function VpnPage() {
const [showAddDevice, setShowAddDevice] = useState(false)
const [showConfig, setShowConfig] = useState<string | null>(null)
const [endpoint, setEndpoint] = useState('')
const [selectedDevice, setSelectedDevice] = useState('')
const [copied, setCopied] = useState(false)
@@ -83,7 +77,11 @@ export function VpnPage() {
const { data: devices = [] } = useQuery({
queryKey: ['devices', tenantId],
queryFn: () => devicesApi.list(tenantId).then((r: any) => r.items ?? r.devices ?? []),
queryFn: () => devicesApi.list(tenantId).then((r: unknown) => {
const result = r as { items?: DeviceResponse[]; devices?: DeviceResponse[] } | DeviceResponse[]
if (Array.isArray(result)) return result
return result.items ?? result.devices ?? []
}),
enabled: !!tenantId && showAddDevice,
})
@@ -96,12 +94,15 @@ export function VpnPage() {
// ── Mutations ──
const setupMutation = useMutation({
mutationFn: () => vpnApi.setup(tenantId, endpoint || undefined),
mutationFn: () => vpnApi.setup(tenantId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
toast({ title: 'VPN enabled successfully' })
},
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to enable VPN', variant: 'destructive' }),
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({ title: err?.response?.data?.detail || 'Failed to enable VPN', variant: 'destructive' })
},
})
const addPeerMutation = useMutation({
@@ -113,7 +114,10 @@ export function VpnPage() {
setSelectedDevice('')
toast({ title: 'Device added to VPN' })
},
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to add device', variant: 'destructive' }),
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({ title: err?.response?.data?.detail || 'Failed to add device', variant: 'destructive' })
},
})
const removePeerMutation = useMutation({
@@ -123,7 +127,10 @@ export function VpnPage() {
queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
toast({ title: 'Device removed from VPN' })
},
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to remove device', variant: 'destructive' }),
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({ title: err?.response?.data?.detail || 'Failed to remove device', variant: 'destructive' })
},
})
const toggleMutation = useMutation({
@@ -141,7 +148,10 @@ export function VpnPage() {
queryClient.invalidateQueries({ queryKey: ['vpn-peers'] })
toast({ title: 'VPN configuration deleted' })
},
onError: (e: any) => toast({ title: e?.response?.data?.detail || 'Failed to delete VPN', variant: 'destructive' }),
onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({ title: err?.response?.data?.detail || 'Failed to delete VPN', variant: 'destructive' })
},
})
// ── Helpers ──

View File

@@ -27,6 +27,7 @@ export function EventStreamProvider({
)
}
// eslint-disable-next-line react-refresh/only-export-components
export function useEventStreamContext() {
return useContext(EventStreamContext)
}

View File

@@ -6,7 +6,9 @@ import { useEffect, useRef } from 'react'
*/
export function useShortcut(key: string, callback: () => void, enabled = true) {
const callbackRef = useRef(callback)
callbackRef.current = callback
useEffect(() => {
callbackRef.current = callback
})
useEffect(() => {
if (!enabled) return
@@ -43,7 +45,9 @@ export function useSequenceShortcut(
enabled = true,
) {
const callbackRef = useRef(callback)
callbackRef.current = callback
useEffect(() => {
callbackRef.current = callback
})
const pendingRef = useRef<string | null>(null)
const timeoutRef = useRef<number | null>(null)

View File

@@ -78,8 +78,9 @@ export const certificatesApi = {
params: tenantParams(tenantId),
})
return data
} catch (err: any) {
if (err?.response?.status === 404) return null
} catch (err: unknown) {
const e = err as { response?: { status?: number } }
if (e?.response?.status === 404) return null
throw err
}
},

View File

@@ -29,7 +29,6 @@ const N_HEX =
const N = BigInt('0x' + N_HEX);
const g = 2n;
const N_BYTES = 256; // 2048 bits = 256 bytes
const N_HEX_LEN = N_BYTES * 2; // 512 hex chars
// ---- Utility Functions ----
@@ -39,16 +38,6 @@ function toHex(n: bigint): string {
return hex;
}
/** Pad a hex string to N's byte length (512 hex chars) with leading zeros. */
function padHex(hex: string): string {
return hex.padStart(N_HEX_LEN, '0');
}
/** Convert BigInt to padded hex bytes (for hash inputs involving N-sized values). */
function bigintToPaddedHex(n: bigint): string {
return padHex(toHex(n));
}
/** Convert hex string to Uint8Array. */
function hexToBytes(hex: string): Uint8Array {
const padded = hex.length % 2 === 1 ? '0' + hex : hex;
@@ -93,12 +82,6 @@ function bigintToBytes(n: bigint): Uint8Array {
return hexToBytes(toHex(n));
}
/** Hash BigInt values (unpadded, matching srptools int_to_bytes) and return bytes. */
async function hashBigInt(...values: bigint[]): Promise<Uint8Array> {
const inputs = values.map((v) => bigintToBytes(v));
return H(...inputs);
}
/** Pad a BigInt value to N's byte length (256 bytes) matching srptools context.pad(). */
function padBigInt(n: bigint): Uint8Array {
const bytes = bigintToBytes(n);

View File

@@ -336,7 +336,7 @@ function placeFormatInfo(matrix: boolean[][], size: number) {
// After BCH: 0x77c0... Let's use the precomputed value
// EC L = 01, mask 0 = 000 -> data = 01000
// Format string after BCH and XOR with 101010000010010:
const formatBits = 0x77c0 // L, mask 0
// const formatBits = 0x77c0 // L, mask 0 (unused, computed via getFormatInfo below)
// Actually, let's compute it properly
// data = 01 000 = 0b01000 = 8
// Generator: 10100110111 (0x537)

View File

@@ -35,6 +35,7 @@ function DevicesPage() {
// Open dialog when ?add=true is set (e.g. from empty state button)
useEffect(() => {
if (search.add === 'true') {
// eslint-disable-next-line react-hooks/set-state-in-effect
setAddOpen(true)
// Clear the search param so it doesn't re-open on navigation
void navigate({

View File

@@ -35,5 +35,6 @@ export function renderWithProviders(
}
}
// eslint-disable-next-line react-refresh/only-export-components
export * from '@testing-library/react'
export { renderWithProviders as render }