Skip to content

Commit a0dabdf

Browse files
authored
fix(frontend): resolve all ESLint warnings and svelte-check a11y issues (#2432)
Address 1 error and 49 warnings from npm run check:all: - Fix unnecessary optional chain in e2e test - Add eslint-disable comments for security/detect-object-injection with justifications - Fix 6 a11y label-control association warnings in wizard steps - Add id prop passthrough to LanguageSelector component - Suppress detect-unsafe-regex and detect-non-literal-fs-filename warnings Co-authored-by: tphakala <tphakala@users.noreply.github.com>
1 parent 0352a03 commit a0dabdf

24 files changed

Lines changed: 83 additions & 15 deletions

frontend/src/lib/desktop/components/forms/ToggleField.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
md: 'w-12 h-6 before:w-5 before:h-5 checked:before:translate-x-6',
6060
};
6161
62+
// eslint-disable-next-line security/detect-object-injection -- size is typed as 'sm' | 'md'
6263
let toggleBaseClasses = $derived(`${toggleSharedClasses} ${toggleSizeClasses[size]}`);
6364
6465
const toggleErrorClasses = 'checked:bg-[var(--color-error)]';

frontend/src/lib/desktop/components/media/AudioPlayer.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,7 @@
692692
693693
// Map format to container file extension (must match backend clipFileExtension)
694694
const extMap: Record<string, string> = { alac: 'm4a', aac: 'm4a', opus: 'ogg' };
695+
// eslint-disable-next-line security/detect-object-injection -- extractionFormat is a controlled string from component props
695696
const ext = extMap[extractionFormat] ?? extractionFormat;
696697
// Sanitize label for filesystem safety (remove reserved chars, normalize whitespace)
697698
const safeLabel = clipLabel

frontend/src/lib/desktop/components/media/SpectrogramCanvas.svelte

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,15 @@
8686
const freqRatio = 1 - y / (height - 1 || 1);
8787
const freq = minFreq + freqRatio * (maxFreq - minFreq);
8888
const binIndex = Math.round((freq / nyquist) * (binCount - 1));
89+
// eslint-disable-next-line security/detect-object-injection -- y is a loop counter bounded by canvas height
8990
map[y] = Math.max(0, Math.min(binCount - 1, binIndex));
9091
}
9192
9293
return map;
9394
});
9495
9596
// Selected color LUT
97+
// eslint-disable-next-line security/detect-object-injection -- colorMap is typed as ColorMapName
9698
let colorLUT = $derived(COLOR_MAPS[colorMap] ?? COLOR_MAPS[DEFAULT_COLOR_MAP]);
9799
98100
// ResizeObserver with debouncing (100ms)
@@ -248,9 +250,11 @@
248250
249251
for (let col = 0; col < pixelsToScroll; col++) {
250252
for (let y = 0; y < h; y++) {
253+
/* eslint-disable security/detect-object-injection -- loop indices and typed array lookups */
251254
const binIndex = currentBinMap[y];
252255
const magnitude = frequencyData[binIndex];
253256
data[y * pixelsToScroll + col] = currentLUT[magnitude];
257+
/* eslint-enable security/detect-object-injection */
254258
}
255259
}
256260

frontend/src/lib/desktop/components/ui/LanguageSelector.svelte

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
// Props
99
interface Props {
1010
className?: string;
11+
id?: string;
1112
}
1213
13-
let { className = '' }: Props = $props();
14+
let { className = '', id }: Props = $props();
1415
1516
// Extended option type for locale with typed locale code
1617
interface LocaleOption extends SelectOption {
@@ -55,6 +56,7 @@
5556
size="sm"
5657
groupBy={false}
5758
{className}
59+
{id}
5860
onChange={handleLanguageChange}
5961
>
6062
{#snippet renderOption(option)}

frontend/src/lib/desktop/components/ui/Modal.svelte

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,12 @@
231231
>
232232
<div
233233
bind:this={modalElement}
234-
class={cn(sizeClasses[size], { 'scale-100': isOpen }, className)}
234+
class={cn(
235+
// eslint-disable-next-line security/detect-object-injection -- size is typed as ModalSize
236+
sizeClasses[size],
237+
{ 'scale-100': isOpen },
238+
className
239+
)}
235240
role="document"
236241
tabindex="-1"
237242
>
@@ -281,7 +286,11 @@
281286
</button>
282287
<button
283288
type="button"
284-
class={cn(btnBase, confirmButtonStyles[confirmVariant])}
289+
class={cn(
290+
btnBase,
291+
// eslint-disable-next-line security/detect-object-injection -- confirmVariant is typed union
292+
confirmButtonStyles[confirmVariant]
293+
)}
285294
onclick={handleConfirm}
286295
disabled={loading || isConfirming}
287296
>

frontend/src/lib/desktop/components/ui/StatusBanner.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@
2525
info: 'bg-[var(--color-info)]/15 text-[var(--color-info)]',
2626
warning: 'bg-[var(--color-warning)]/15 text-[var(--color-warning)]',
2727
};
28+
29+
// eslint-disable-next-line security/detect-object-injection -- type is typed as StatusType
30+
let typeStyle = $derived(styleMap[type]);
2831
</script>
2932

3033
<div
31-
class="flex items-center gap-2 rounded-lg p-3 text-sm {styleMap[type]} {className}"
34+
class="flex items-center gap-2 rounded-lg p-3 text-sm {typeStyle} {className}"
3235
role={type === 'error' ? 'alert' : 'status'}
3336
aria-live={type === 'error' ? 'assertive' : 'polite'}
3437
>

frontend/src/lib/desktop/components/ui/WeatherSvgIcon.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
8585
let { icon, size = 32, className = '', title = '' }: Props = $props();
8686
87+
// eslint-disable-next-line security/detect-object-injection -- icon is a string prop used as key in a static icon map
8788
let svgContent = $derived(iconMap[icon] ?? iconMap['not-available']);
8889
</script>
8990

frontend/src/lib/desktop/features/dashboard/components/CurrentlyHearingCard.svelte

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ Props:
4444
for (const d of detections) {
4545
const key = detectionKey(d);
4646
if ((d.status === 'approved' || d.status === 'rejected') && !(key in removalTimers)) {
47+
/* eslint-disable security/detect-object-injection -- key is derived from detectionKey(), a controlled string */
4748
retainedData[key] = d;
4849
removalTimers[key] = setTimeout(() => {
4950
delete retainedData[key];
5051
delete removalTimers[key];
52+
/* eslint-enable security/detect-object-injection */
5153
retainedKeys = retainedKeys.filter(k => k !== key);
5254
}, TERMINAL_RETENTION_MS);
5355
if (!untrack(() => retainedKeys).includes(key)) {
@@ -70,6 +72,7 @@ Props:
7072
const result: PendingDetection[] = [...detections];
7173
for (const key of retained) {
7274
if (!incomingByKey.has(key)) {
75+
// eslint-disable-next-line security/detect-object-injection -- key is from retainedKeys, a controlled string array
7376
const data = retainedData[key];
7477
if (data) {
7578
result.push(data);
@@ -114,13 +117,19 @@ Props:
114117
return result;
115118
});
116119
120+
function getElapsedForKey(key: string): string {
121+
// eslint-disable-next-line security/detect-object-injection -- key is a controlled detection key string
122+
return elapsedTexts[key] ?? '';
123+
}
124+
117125
// Show source column only when multiple sources are present
118126
let hasMultipleSources = $derived(new Set(displayDetections.map(d => d.source)).size > 1);
119127
120128
// Clean up pending timers on component destroy
121129
$effect(() => {
122130
return () => {
123131
for (const key in removalTimers) {
132+
// eslint-disable-next-line security/detect-object-injection -- key is from for-in over own Record
124133
clearTimeout(removalTimers[key]);
125134
}
126135
};
@@ -145,6 +154,7 @@ Props:
145154
<div class="flex flex-wrap gap-3 p-4">
146155
{#each displayDetections as detection (`${detection.source}_${detection.scientificName}`)}
147156
{@const key = detection.source + detection.scientificName}
157+
{@const elapsedText = getElapsedForKey(key)}
148158
<div
149159
class="flex items-center gap-2 rounded-lg px-3 py-2 transition-colors duration-300
150160
{detection.status === 'approved'
@@ -175,7 +185,7 @@ Props:
175185
{detection.species}
176186
</span>
177187
<span class="text-xs text-[var(--color-base-content)]/60">
178-
{elapsedTexts[key] ?? ''}
188+
{elapsedText}
179189
{#if hasMultipleSources}
180190
· {detection.source}
181191
{/if}

frontend/src/lib/desktop/features/dashboard/components/DashboardEditMode.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
let missingTypes = $derived(
6868
ALL_ELEMENT_TYPES.filter(type => {
6969
const count = editElements.filter(el => el.type === type).length;
70+
// eslint-disable-next-line security/detect-object-injection -- type is from ALL_ELEMENT_TYPES, a typed constant array
7071
const max = MAX_INSTANCES[type] ?? 1;
7172
return count < max;
7273
})

frontend/src/lib/desktop/features/dashboard/components/MiniSpectrogram.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
let showDetectionLabels = $state(true);
6464
6565
const gainLabel = $derived.by(() => {
66+
// eslint-disable-next-line security/detect-object-injection -- gainPresetIndex is a numeric index bounded by GAIN_PRESETS.length
6667
const preset = GAIN_PRESETS[gainPresetIndex];
6768
return 'value' in preset ? t(preset.labelKey, { value: preset.value }) : t(preset.labelKey);
6869
});
@@ -378,6 +379,7 @@
378379
379380
function cycleVolume() {
380381
gainPresetIndex = (gainPresetIndex + 1) % GAIN_PRESETS.length;
382+
// eslint-disable-next-line security/detect-object-injection -- gainPresetIndex is a numeric index bounded by modulo
381383
const preset = GAIN_PRESETS[gainPresetIndex];
382384
spectro.setAudioOutput(preset.audio);
383385
if (preset.audio) {

0 commit comments

Comments
 (0)