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, DialogDescription,
} from '@/components/ui/dialog' } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button' 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 { keyStore } from '@/lib/crypto/keyStore'
import { authApi } from '@/lib/api' import { authApi } from '@/lib/api'
import { EmergencyKitDialog } from './EmergencyKitDialog' import { EmergencyKitDialog } from './EmergencyKitDialog'

View File

@@ -56,7 +56,7 @@ export function BulkDeployDialog({
queryKey: ['devices-for-cert', tenantId], queryKey: ['devices-for-cert', tenantId],
queryFn: async () => { queryFn: async () => {
const result = await devicesApi.list(tenantId) const result = await devicesApi.list(tenantId)
return (result as any).items ?? result return (result as { items?: DeviceResponse[] }).items ?? (result as DeviceResponse[])
}, },
enabled: !!tenantId && open, enabled: !!tenantId && open,
}) })
@@ -128,14 +128,15 @@ export function BulkDeployDialog({
variant: 'destructive', variant: 'destructive',
}) })
} }
} catch (e: any) { } catch (e: unknown) {
const err = e as { response?: { data?: { detail?: string } } }
setResult({ setResult({
success: 0, success: 0,
failed: selected.size, failed: selected.size,
errors: [ errors: [
{ {
device_id: 'bulk', 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'] }) void queryClient.invalidateQueries({ queryKey: ['ca'] })
toast({ title: 'Certificate Authority initialized' }) toast({ title: 'Certificate Authority initialized' })
}, },
onError: (e: any) => onError: (e: unknown) => {
const err = e as { response?: { data?: { detail?: string } } }
toast({ toast({
title: e?.response?.data?.detail || 'Failed to initialize CA', title: err?.response?.data?.detail || 'Failed to initialize CA',
variant: 'destructive', variant: 'destructive',
}), })
},
}) })
const handleDownloadPEM = async () => { const handleDownloadPEM = async () => {

View File

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

View File

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

View File

@@ -58,7 +58,7 @@ export function DeployCertDialog({
queryFn: async () => { queryFn: async () => {
const result = await devicesApi.list(tenantId) const result = await devicesApi.list(tenantId)
// The list endpoint returns { items, total, ... } or an array // 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, enabled: !!tenantId && open,
}) })
@@ -110,9 +110,10 @@ export function DeployCertDialog({
setErrorMsg(result.error ?? 'Deployment failed') setErrorMsg(result.error ?? 'Deployment failed')
toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' }) toast({ title: result.error ?? 'Deployment failed', variant: 'destructive' })
} }
} catch (e: any) { } catch (e: unknown) {
setStep('error') 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) setErrorMsg(detail)
toast({ title: detail, variant: 'destructive' }) toast({ title: detail, variant: 'destructive' })
} }

View File

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

View File

@@ -7,10 +7,10 @@
* users can browse RouterOS menu paths and manage entries. * 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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { Terminal, ChevronRight, Loader2, WifiOff, Building2 } from 'lucide-react' 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 { metricsApi } from '@/lib/api'
import { useAuth, isSuperAdmin } from '@/lib/auth' import { useAuth, isSuperAdmin } from '@/lib/auth'
import { useUIStore } from '@/lib/store' import { useUIStore } from '@/lib/store'

View File

@@ -3,7 +3,7 @@
* in a dynamic table with edit/delete action buttons. * 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 { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { canWrite } from '@/lib/auth' import { canWrite } from '@/lib/auth'
@@ -19,8 +19,6 @@ interface EntryTableProps {
onAdd: () => void 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({ export function EntryTable({
entries, entries,

View File

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

View File

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

View File

@@ -48,12 +48,6 @@ export function BridgeVlanPanel({ tenantId, deviceId, active }: ConfigPanelProps
[ports.entries], [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(() => { const handleAdd = useCallback(() => {
setEditEntry(null) setEditEntry(null)
setFormData({ setFormData({

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
*/ */
import { useState, useCallback, useMemo } from 'react' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'

View File

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

View File

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

View File

@@ -6,11 +6,10 @@
*/ */
import { useState, useCallback, useMemo } from 'react' 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 { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Badge } from '@/components/ui/badge'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,

View File

@@ -29,15 +29,6 @@ interface TorchEntry {
rx: string 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 { function formatBps(val: string): string {
const n = parseInt(val, 10) const n = parseInt(val, 10)
if (isNaN(n)) return val || '-' if (isNaN(n)) return val || '-'

View File

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

View File

@@ -51,7 +51,8 @@ export function AlertSummary({
criticalCount, criticalCount,
warningCount, warningCount,
infoCount, infoCount,
tenantId, // eslint-disable-next-line @typescript-eslint/no-unused-vars
tenantId: _tenantId,
}: AlertSummaryProps) { }: AlertSummaryProps) {
const total = criticalCount + warningCount + infoCount const total = criticalCount + warningCount + infoCount
const counts: Record<string, number> = { 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% * - 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+ * - 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( export function computeHealthScore(
devices: HealthScoreProps['devices'], devices: HealthScoreProps['devices'],
criticalAlerts: number, criticalAlerts: number,
@@ -115,7 +116,6 @@ const CIRCUMFERENCE = 2 * Math.PI * RADIUS
export function HealthScore({ export function HealthScore({
devices, devices,
activeAlerts: _activeAlerts,
criticalAlerts, criticalAlerts,
}: HealthScoreProps) { }: HealthScoreProps) {
const totalDevices = devices.length const totalDevices = devices.length

View File

@@ -14,6 +14,7 @@ export interface KpiCardsProps {
* Formats bytes-per-second into a human-readable bandwidth string. * Formats bytes-per-second into a human-readable bandwidth string.
* Auto-scales through bps, Kbps, Mbps, Gbps. * 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 } { export function formatBandwidth(bps: number): { value: number; unit: string } {
if (bps < 1_000) return { value: bps, unit: 'bps' } if (bps < 1_000) return { value: bps, unit: 'bps' }
if (bps < 1_000_000) return { value: bps / 1_000, unit: 'Kbps' } if (bps < 1_000_000) return { value: bps / 1_000, unit: 'Kbps' }

View File

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

View File

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

View File

@@ -8,7 +8,7 @@
* Step 5: Import & Verify (bulk-add, then check connectivity) * 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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useNavigate } from '@tanstack/react-router' import { useNavigate } from '@tanstack/react-router'
import { import {
@@ -31,8 +31,6 @@ import {
type SubnetScanResponse, type SubnetScanResponse,
type SubnetScanResult, type SubnetScanResult,
type DeviceResponse, type DeviceResponse,
type DeviceGroupResponse,
type DeviceTagResponse,
} from '@/lib/api' } from '@/lib/api'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' 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', (s) => s.status === 'active' || s.status === 'creating',
) )
if (active) { if (active) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setSession(active) setSession(active)
// eslint-disable-next-line react-hooks/set-state-in-effect
setState(active.status === 'active' ? 'active' : 'connecting') setState(active.status === 'active' ? 'active' : 'connecting')
} }
} }
@@ -66,6 +68,7 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
// Countdown timer for session expiry // Countdown timer for session expiry
useEffect(() => { useEffect(() => {
if (state !== 'active' || !session?.expires_at) { if (state !== 'active' || !session?.expires_at) {
// eslint-disable-next-line react-hooks/set-state-in-effect
setCountdown(null) setCountdown(null)
return return
} }
@@ -96,9 +99,10 @@ export function RemoteWinBoxButton({ tenantId, deviceId }: RemoteWinBoxButtonPro
setState('connecting') setState('connecting')
} }
}, },
onError: (err: any) => { onError: (err: unknown) => {
const e = err as { response?: { data?: { detail?: string } } }
setState('failed') 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) setError(null)
queryClient.invalidateQueries({ queryKey: ['remote-winbox-sessions', tenantId, deviceId] }) queryClient.invalidateQueries({ queryKey: ['remote-winbox-sessions', tenantId, deviceId] })
}, },
onError: (err: any) => { onError: (err: unknown) => {
const e = err as { response?: { data?: { detail?: string } } }
setState('failed') 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.mutate()
}, [closeMutation]) }, [closeMutation])
const handleRetry = useCallback(() => {
setSession(null)
setError(null)
handleOpen()
}, [handleOpen])
const handleReset = useCallback(async () => { const handleReset = useCallback(async () => {
try { try {
const sessions = await remoteWinboxApi.list(tenantId, deviceId) 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') window.open(data.winbox_uri, '_blank')
} }
}, },
onError: (err: any) => { onError: (err: unknown) => {
const e = err as { response?: { data?: { detail?: string } } }
setState('error') 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 ?? [] 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() { function resetForm() {
setName('') setName('')
setStartAt('') setStartAt('')
@@ -94,6 +77,25 @@ export function MaintenanceForm({
return local.toISOString().slice(0, 16) 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({ const createMutation = useMutation({
mutationFn: (data: MaintenanceWindowCreate) => mutationFn: (data: MaintenanceWindowCreate) =>
maintenanceApi.create(tenantId, data), 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. * 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( export function getTimeRange(
range: string, range: string,
customStart?: string, customStart?: string,
@@ -64,6 +65,7 @@ export function getTimeRange(
* Returns refetchInterval (ms) for short ranges, false for longer ones. * Returns refetchInterval (ms) for short ranges, false for longer ones.
* Per user decision: 1h and 6h auto-refresh every 60 seconds. * 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 { export function shouldAutoRefresh(range: string): number | false {
if (range === '1h' || range === '6h') return 60_000 if (range === '1h' || range === '6h') return 60_000
return false return false

View File

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

View File

@@ -53,7 +53,7 @@ interface GaugeBarProps {
direction: 'RX' | 'TX' 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 pct = Math.min((value / maxSpeed) * 100, 100)
const colorClass = getBarColor(pct) const colorClass = getBarColor(pct)

View File

@@ -149,7 +149,7 @@ interface TooltipData {
y: number y: number
} }
function NodeTooltip({ data, onClose }: { data: TooltipData; onClose: () => void }) { function NodeTooltip({ data }: { data: TooltipData; onClose?: () => void }) {
return ( return (
<div <div
className="absolute z-50 rounded-lg border border-border bg-elevated shadow-lg px-3 py-2 text-xs pointer-events-none" 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, devicesApi,
deviceGroupsApi, deviceGroupsApi,
type DeviceResponse, type DeviceResponse,
type DeviceGroupResponse,
} from '@/lib/api' } from '@/lib/api'
import { configEditorApi } from '@/lib/configEditorApi' import { configEditorApi } from '@/lib/configEditorApi'
import { Button } from '@/components/ui/button' 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 { authApi } from '@/lib/api'
import { getSMTPSettings, updateSMTPSettings, testSMTPSettings, clearWinboxSessions } from '@/lib/settingsApi' import { getSMTPSettings, updateSMTPSettings, testSMTPSettings, clearWinboxSessions } from '@/lib/settingsApi'
import { SMTP_PRESETS } from '@/lib/smtpPresets' 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 { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -365,8 +365,9 @@ function SMTPSettingsSection() {
if (result.success) { if (result.success) {
saveMutation.mutate() saveMutation.mutate()
} }
} catch (e: any) { } catch (e: unknown) {
setTestResult({ success: false, message: e.response?.data?.message || e.message }) const err = e as { response?: { data?: { message?: string } }; message?: string }
setTestResult({ success: false, message: err.response?.data?.message || err.message || 'Unknown error' })
} finally { } finally {
setTesting(false) setTesting(false)
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ interface TemplateEditorProps {
const VARIABLE_TYPES = ['string', 'ip', 'integer', 'boolean', 'subnet'] as const 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 [name, setName] = useState(template?.name ?? '')
const [description, setDescription] = useState(template?.description ?? '') const [description, setDescription] = useState(template?.description ?? '')
const [content, setContent] = useState(template?.content ?? '') 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)) ?? [] const selectedDevices = devices?.filter((d) => selectedDeviceIds.has(d.id)) ?? []
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleGroupSelect = (_groupId: string) => { const handleGroupSelect = (_groupId: string) => {
// For now, just select all online devices. In a real implementation, // For now, just select all online devices. In a real implementation,
// we'd load group members from the API. Here we select all devices // 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' Button.displayName = 'Button'
// eslint-disable-next-line react-refresh/only-export-components
export { Button, buttonVariants } export { Button, buttonVariants }

View File

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

View File

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

View File

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

View File

@@ -26,15 +26,10 @@ import {
import { import {
vpnApi, vpnApi,
devicesApi, devicesApi,
type VpnConfigResponse,
type VpnPeerResponse,
type VpnPeerConfig,
type DeviceResponse, type DeviceResponse,
} from '@/lib/api' } from '@/lib/api'
import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth' import { useAuth, isSuperAdmin, canWrite } from '@/lib/auth'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -63,7 +58,6 @@ export function VpnPage() {
const [showAddDevice, setShowAddDevice] = useState(false) const [showAddDevice, setShowAddDevice] = useState(false)
const [showConfig, setShowConfig] = useState<string | null>(null) const [showConfig, setShowConfig] = useState<string | null>(null)
const [endpoint, setEndpoint] = useState('')
const [selectedDevice, setSelectedDevice] = useState('') const [selectedDevice, setSelectedDevice] = useState('')
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
@@ -83,7 +77,11 @@ export function VpnPage() {
const { data: devices = [] } = useQuery({ const { data: devices = [] } = useQuery({
queryKey: ['devices', tenantId], 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, enabled: !!tenantId && showAddDevice,
}) })
@@ -96,12 +94,15 @@ export function VpnPage() {
// ── Mutations ── // ── Mutations ──
const setupMutation = useMutation({ const setupMutation = useMutation({
mutationFn: () => vpnApi.setup(tenantId, endpoint || undefined), mutationFn: () => vpnApi.setup(tenantId),
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['vpn-config'] }) queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
toast({ title: 'VPN enabled successfully' }) 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({ const addPeerMutation = useMutation({
@@ -113,7 +114,10 @@ export function VpnPage() {
setSelectedDevice('') setSelectedDevice('')
toast({ title: 'Device added to VPN' }) 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({ const removePeerMutation = useMutation({
@@ -123,7 +127,10 @@ export function VpnPage() {
queryClient.invalidateQueries({ queryKey: ['vpn-config'] }) queryClient.invalidateQueries({ queryKey: ['vpn-config'] })
toast({ title: 'Device removed from VPN' }) 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({ const toggleMutation = useMutation({
@@ -141,7 +148,10 @@ export function VpnPage() {
queryClient.invalidateQueries({ queryKey: ['vpn-peers'] }) queryClient.invalidateQueries({ queryKey: ['vpn-peers'] })
toast({ title: 'VPN configuration deleted' }) 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 ── // ── Helpers ──

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,6 @@ const N_HEX =
const N = BigInt('0x' + N_HEX); const N = BigInt('0x' + N_HEX);
const g = 2n; const g = 2n;
const N_BYTES = 256; // 2048 bits = 256 bytes const N_BYTES = 256; // 2048 bits = 256 bytes
const N_HEX_LEN = N_BYTES * 2; // 512 hex chars
// ---- Utility Functions ---- // ---- Utility Functions ----
@@ -39,16 +38,6 @@ function toHex(n: bigint): string {
return hex; 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. */ /** Convert hex string to Uint8Array. */
function hexToBytes(hex: string): Uint8Array { function hexToBytes(hex: string): Uint8Array {
const padded = hex.length % 2 === 1 ? '0' + hex : hex; const padded = hex.length % 2 === 1 ? '0' + hex : hex;
@@ -93,12 +82,6 @@ function bigintToBytes(n: bigint): Uint8Array {
return hexToBytes(toHex(n)); 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(). */ /** Pad a BigInt value to N's byte length (256 bytes) matching srptools context.pad(). */
function padBigInt(n: bigint): Uint8Array { function padBigInt(n: bigint): Uint8Array {
const bytes = bigintToBytes(n); 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 // After BCH: 0x77c0... Let's use the precomputed value
// EC L = 01, mask 0 = 000 -> data = 01000 // EC L = 01, mask 0 = 000 -> data = 01000
// Format string after BCH and XOR with 101010000010010: // 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 // Actually, let's compute it properly
// data = 01 000 = 0b01000 = 8 // data = 01 000 = 0b01000 = 8
// Generator: 10100110111 (0x537) // Generator: 10100110111 (0x537)

View File

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