@smartnet360/svelte-components 0.0.123 → 0.0.124
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/antenna-tools/components/AntennaControls.svelte +71 -9
- package/dist/apps/antenna-tools/components/AntennaControls.svelte.d.ts +2 -0
- package/dist/apps/antenna-tools/components/AntennaTools.svelte +48 -82
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +3 -6
- package/dist/apps/antenna-tools/components/MSIConverter.svelte +75 -9
- package/dist/apps/antenna-tools/utils/msi-parser.d.ts +35 -1
- package/dist/apps/antenna-tools/utils/msi-parser.js +105 -35
- package/dist/core/Benchmark/Benchmark.svelte +662 -0
- package/dist/core/Benchmark/Benchmark.svelte.d.ts +3 -0
- package/dist/core/Benchmark/benchmark-utils.d.ts +48 -0
- package/dist/core/Benchmark/benchmark-utils.js +80 -0
- package/dist/core/Benchmark/index.d.ts +2 -0
- package/dist/core/Benchmark/index.js +3 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/package.json +1 -1
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
onElectricalTiltChange?: (index: number) => void;
|
|
16
16
|
onMechanicalTiltChange?: (tilt: number) => void;
|
|
17
17
|
showStatus?: boolean;
|
|
18
|
+
/** Requested frequency from external mode - will be highlighted */
|
|
19
|
+
requestedFrequency?: number | null;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
let {
|
|
@@ -27,7 +29,8 @@
|
|
|
27
29
|
onAntennaChange,
|
|
28
30
|
onElectricalTiltChange,
|
|
29
31
|
onMechanicalTiltChange,
|
|
30
|
-
showStatus = true
|
|
32
|
+
showStatus = true,
|
|
33
|
+
requestedFrequency = null
|
|
31
34
|
}: Props = $props();
|
|
32
35
|
|
|
33
36
|
// Internal state for slider values
|
|
@@ -36,6 +39,20 @@
|
|
|
36
39
|
let internalSelectedAntenna = $state(selectedAntenna);
|
|
37
40
|
let selectedFrequency = $state<number | null>(null);
|
|
38
41
|
|
|
42
|
+
// Check if requested frequency is available for the selected antenna
|
|
43
|
+
let frequencyError = $derived.by(() => {
|
|
44
|
+
if (!requestedFrequency || !internalSelectedAntenna) return null;
|
|
45
|
+
|
|
46
|
+
// Get all frequencies available for this antenna
|
|
47
|
+
const sameNameAntennas = antennas.filter(a => a.name === internalSelectedAntenna!.name);
|
|
48
|
+
const availableFreqs = [...new Set(sameNameAntennas.map(a => a.frequency))];
|
|
49
|
+
|
|
50
|
+
if (!availableFreqs.includes(requestedFrequency)) {
|
|
51
|
+
return `Requested frequency ${requestedFrequency} MHz is not available for this antenna. Available: ${availableFreqs.sort((a, b) => a - b).join(', ')} MHz`;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
});
|
|
55
|
+
|
|
39
56
|
// Update internal state when props change
|
|
40
57
|
$effect(() => {
|
|
41
58
|
internalElectricalTiltIndex = electricalTiltIndex;
|
|
@@ -305,18 +322,36 @@
|
|
|
305
322
|
return colors[theme];
|
|
306
323
|
}
|
|
307
324
|
|
|
325
|
+
// Type for frequency info with original frequency
|
|
326
|
+
interface FrequencyInfo {
|
|
327
|
+
band: number;
|
|
328
|
+
originalFrequency: number;
|
|
329
|
+
}
|
|
330
|
+
|
|
308
331
|
// Get unique frequency bands for the selected antenna name
|
|
309
|
-
let uniqueFrequencies = $state<
|
|
332
|
+
let uniqueFrequencies = $state<FrequencyInfo[]>([]);
|
|
310
333
|
$effect(() => {
|
|
311
334
|
if (antennas.length > 0 && internalSelectedAntenna) {
|
|
312
335
|
// Get frequencies only for the selected antenna name
|
|
313
336
|
const sameNameAntennas = antennas.filter(a => a.name === internalSelectedAntenna!.name);
|
|
314
|
-
|
|
337
|
+
|
|
338
|
+
// Create unique frequency entries with original frequency
|
|
339
|
+
const freqMap = new Map<number, number>();
|
|
340
|
+
sameNameAntennas.forEach(a => {
|
|
341
|
+
// Keep the first original frequency for each band
|
|
342
|
+
if (!freqMap.has(a.frequency)) {
|
|
343
|
+
freqMap.set(a.frequency, a.originalFrequency);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
const frequencies = Array.from(freqMap.entries())
|
|
348
|
+
.map(([band, orig]) => ({ band, originalFrequency: orig }))
|
|
349
|
+
.sort((a, b) => a.band - b.band);
|
|
315
350
|
uniqueFrequencies = frequencies;
|
|
316
351
|
|
|
317
352
|
// Auto-select frequency if there's only one option
|
|
318
353
|
if (frequencies.length === 1) {
|
|
319
|
-
selectedFrequency = frequencies[0];
|
|
354
|
+
selectedFrequency = frequencies[0].band;
|
|
320
355
|
}
|
|
321
356
|
} else {
|
|
322
357
|
uniqueFrequencies = [];
|
|
@@ -348,10 +383,14 @@
|
|
|
348
383
|
}
|
|
349
384
|
});
|
|
350
385
|
|
|
351
|
-
// Format frequency for display (
|
|
386
|
+
// Format frequency for display (band [original] -> "700 [712]")
|
|
352
387
|
function formatFrequency(freq: number): string {
|
|
353
388
|
return `${freq} MHz`;
|
|
354
389
|
}
|
|
390
|
+
|
|
391
|
+
function formatFrequencyWithOriginal(info: FrequencyInfo): string {
|
|
392
|
+
return `${info.band} [${info.originalFrequency}]`;
|
|
393
|
+
}
|
|
355
394
|
</script>
|
|
356
395
|
|
|
357
396
|
<div class="card h-100">
|
|
@@ -396,24 +435,47 @@
|
|
|
396
435
|
{/if}
|
|
397
436
|
</div>
|
|
398
437
|
|
|
438
|
+
<!-- Frequency Error Message -->
|
|
439
|
+
{#if frequencyError}
|
|
440
|
+
<div class="alert alert-danger py-2 mb-3" role="alert">
|
|
441
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
442
|
+
<small>{frequencyError}</small>
|
|
443
|
+
</div>
|
|
444
|
+
{/if}
|
|
445
|
+
|
|
399
446
|
<!-- Frequency Band Filter -->
|
|
400
447
|
{#if uniqueFrequencies.length > 1}
|
|
401
448
|
<div class="mb-3">
|
|
402
449
|
<div class="form-label">
|
|
403
450
|
<strong>Frequency Band:</strong>
|
|
451
|
+
{#if requestedFrequency && !frequencyError}
|
|
452
|
+
<span class="badge bg-info ms-2">Requested: {requestedFrequency} MHz</span>
|
|
453
|
+
{/if}
|
|
404
454
|
</div>
|
|
405
455
|
<div class="d-flex flex-wrap gap-1">
|
|
406
|
-
{#each uniqueFrequencies as
|
|
456
|
+
{#each uniqueFrequencies as freqInfo}
|
|
457
|
+
{@const isRequested = requestedFrequency === freqInfo.band}
|
|
458
|
+
{@const isSelected = selectedFrequency === freqInfo.band}
|
|
407
459
|
<button
|
|
408
460
|
type="button"
|
|
409
|
-
class="btn btn-sm {
|
|
410
|
-
|
|
461
|
+
class="btn btn-sm {isSelected ? 'btn-primary' : isRequested ? 'btn-outline-info border-2' : 'btn-outline-secondary'}"
|
|
462
|
+
class:fw-bold={isRequested}
|
|
463
|
+
onclick={() => handleFrequencyChange(freqInfo.band)}
|
|
411
464
|
>
|
|
412
|
-
{
|
|
465
|
+
{formatFrequencyWithOriginal(freqInfo)}
|
|
466
|
+
{#if isRequested && !isSelected}
|
|
467
|
+
<i class="bi bi-arrow-left-short"></i>
|
|
468
|
+
{/if}
|
|
413
469
|
</button>
|
|
414
470
|
{/each}
|
|
415
471
|
</div>
|
|
416
472
|
</div>
|
|
473
|
+
{:else if uniqueFrequencies.length === 1 && requestedFrequency && uniqueFrequencies[0].band !== requestedFrequency}
|
|
474
|
+
<!-- Single frequency but doesn't match requested -->
|
|
475
|
+
<div class="alert alert-warning py-2 mb-3" role="alert">
|
|
476
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
|
477
|
+
<small>Requested {requestedFrequency} MHz, but only {uniqueFrequencies[0].band} MHz available</small>
|
|
478
|
+
</div>
|
|
417
479
|
{/if}
|
|
418
480
|
|
|
419
481
|
<!-- Electrical Tilt Control -->
|
|
@@ -10,6 +10,8 @@ interface Props {
|
|
|
10
10
|
onElectricalTiltChange?: (index: number) => void;
|
|
11
11
|
onMechanicalTiltChange?: (tilt: number) => void;
|
|
12
12
|
showStatus?: boolean;
|
|
13
|
+
/** Requested frequency from external mode - will be highlighted */
|
|
14
|
+
requestedFrequency?: number | null;
|
|
13
15
|
}
|
|
14
16
|
declare const AntennaControls: import("svelte").Component<Props, {}, "">;
|
|
15
17
|
type AntennaControls = ReturnType<typeof AntennaControls>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<svelte:options runes={true} />
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
|
-
import { onMount } from 'svelte';
|
|
4
|
+
import { onMount, untrack } from 'svelte';
|
|
5
5
|
import { loadAntennas } from '../utils/db-utils';
|
|
6
6
|
import type { Antenna, ExternalAntennaInput, ViewMode, PatternType, ChartEngineType, PatternDisplayMode, BandDefinition } from '../types';
|
|
7
7
|
import AntennaControls from './AntennaControls.svelte';
|
|
@@ -110,12 +110,13 @@
|
|
|
110
110
|
if (externalAntenna1) {
|
|
111
111
|
const found = findAntennaBySpec(antennas, externalAntenna1);
|
|
112
112
|
if (found) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
// Use untrack to prevent infinite loop from state updates
|
|
114
|
+
untrack(() => {
|
|
115
|
+
selectedAntenna = found;
|
|
116
|
+
updateAvailableTilts(found);
|
|
117
|
+
ant1ElectricalTilt = findTiltIndex(availableElectricalTilts, externalAntenna1.electricalTilt ?? 0);
|
|
118
|
+
ant1MechanicalTilt = externalAntenna1.mechanicalTilt ?? 0;
|
|
119
|
+
});
|
|
119
120
|
} else {
|
|
120
121
|
console.warn('[AntennaTools] External antenna 1 not found in database:', externalAntenna1.name);
|
|
121
122
|
}
|
|
@@ -125,13 +126,14 @@
|
|
|
125
126
|
if (externalAntenna2) {
|
|
126
127
|
const found2 = findAntennaBySpec(antennas, externalAntenna2);
|
|
127
128
|
if (found2) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
untrack(() => {
|
|
130
|
+
selectedAntenna2 = found2;
|
|
131
|
+
ant2ElectricalTilt = findTiltIndex(getAvailableTiltsForAntenna(found2), externalAntenna2.electricalTilt ?? 0);
|
|
132
|
+
ant2MechanicalTilt = externalAntenna2.mechanicalTilt ?? 0;
|
|
133
|
+
if (viewMode === 'single') {
|
|
134
|
+
viewMode = 'compare';
|
|
135
|
+
}
|
|
136
|
+
});
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -455,40 +457,22 @@
|
|
|
455
457
|
<div class="row">
|
|
456
458
|
<!-- Left Column - Antenna 1 Controls -->
|
|
457
459
|
<div class="col-md-3">
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
<div class="
|
|
473
|
-
<
|
|
474
|
-
<i class="bi bi-broadcast me-2"></i>
|
|
475
|
-
{antenna1Label || 'Antenna 1'}
|
|
476
|
-
</div>
|
|
477
|
-
<div class="card-body">
|
|
478
|
-
{#if selectedAntenna}
|
|
479
|
-
<h6 class="card-title">{selectedAntenna.name}</h6>
|
|
480
|
-
<div class="small text-muted">
|
|
481
|
-
<div><strong>Band:</strong> {selectedAntenna.frequency}</div>
|
|
482
|
-
<div><strong>E-Tilt:</strong> {availableElectricalTilts[ant1ElectricalTilt] || 0}°</div>
|
|
483
|
-
<div><strong>M-Tilt:</strong> {ant1MechanicalTilt}°</div>
|
|
484
|
-
{#if selectedAntenna.gain_dBd}
|
|
485
|
-
<div><strong>Gain:</strong> {selectedAntenna.gain_dBd} dBd</div>
|
|
486
|
-
{/if}
|
|
487
|
-
</div>
|
|
488
|
-
{:else}
|
|
489
|
-
<p class="text-muted mb-0">Antenna not found in database</p>
|
|
490
|
-
{/if}
|
|
491
|
-
</div>
|
|
460
|
+
<!-- Show full controls in both modes - external mode just pre-selects the antenna -->
|
|
461
|
+
<AntennaControls
|
|
462
|
+
antennas={antennas}
|
|
463
|
+
selectedAntenna={selectedAntenna}
|
|
464
|
+
antennaNumber={1}
|
|
465
|
+
electricalTiltIndex={ant1ElectricalTilt}
|
|
466
|
+
mechanicalTilt={ant1MechanicalTilt}
|
|
467
|
+
colorTheme="primary"
|
|
468
|
+
onAntennaChange={handleAntenna1Change}
|
|
469
|
+
onElectricalTiltChange={handleTilt1Change}
|
|
470
|
+
onMechanicalTiltChange={handleMechTilt1Change}
|
|
471
|
+
requestedFrequency={isExternalMode ? externalAntenna1?.frequency : null}
|
|
472
|
+
/>
|
|
473
|
+
{#if isExternalMode && antenna1Label}
|
|
474
|
+
<div class="badge bg-primary w-100 mt-2 py-2">
|
|
475
|
+
<i class="bi bi-link-45deg me-1"></i>{antenna1Label}
|
|
492
476
|
</div>
|
|
493
477
|
{/if}
|
|
494
478
|
</div>
|
|
@@ -531,40 +515,22 @@
|
|
|
531
515
|
<!-- Right Column - Antenna 2 Controls (only in compare mode) -->
|
|
532
516
|
<div class="col-md-3">
|
|
533
517
|
{#if viewMode === 'compare'}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
<div class="
|
|
549
|
-
<
|
|
550
|
-
<i class="bi bi-broadcast me-2"></i>
|
|
551
|
-
{antenna2Label || 'Antenna 2'}
|
|
552
|
-
</div>
|
|
553
|
-
<div class="card-body">
|
|
554
|
-
{#if selectedAntenna2}
|
|
555
|
-
<h6 class="card-title">{selectedAntenna2.name}</h6>
|
|
556
|
-
<div class="small text-muted">
|
|
557
|
-
<div><strong>Band:</strong> {selectedAntenna2.frequency}</div>
|
|
558
|
-
<div><strong>E-Tilt:</strong> {getAvailableTiltsForAntenna(selectedAntenna2)[ant2ElectricalTilt] || 0}°</div>
|
|
559
|
-
<div><strong>M-Tilt:</strong> {ant2MechanicalTilt}°</div>
|
|
560
|
-
{#if selectedAntenna2.gain_dBd}
|
|
561
|
-
<div><strong>Gain:</strong> {selectedAntenna2.gain_dBd} dBd</div>
|
|
562
|
-
{/if}
|
|
563
|
-
</div>
|
|
564
|
-
{:else}
|
|
565
|
-
<p class="text-muted mb-0">Antenna not found in database</p>
|
|
566
|
-
{/if}
|
|
567
|
-
</div>
|
|
518
|
+
<!-- Show full controls in both modes -->
|
|
519
|
+
<AntennaControls
|
|
520
|
+
antennas={antennas}
|
|
521
|
+
selectedAntenna={selectedAntenna2}
|
|
522
|
+
antennaNumber={2}
|
|
523
|
+
electricalTiltIndex={ant2ElectricalTilt}
|
|
524
|
+
mechanicalTilt={ant2MechanicalTilt}
|
|
525
|
+
colorTheme="warning"
|
|
526
|
+
onAntennaChange={handleAntenna2Change}
|
|
527
|
+
onElectricalTiltChange={handleTilt2Change}
|
|
528
|
+
onMechanicalTiltChange={handleMechTilt2Change}
|
|
529
|
+
requestedFrequency={isExternalMode ? externalAntenna2?.frequency : null}
|
|
530
|
+
/>
|
|
531
|
+
{#if isExternalMode && antenna2Label}
|
|
532
|
+
<div class="badge bg-warning text-dark w-100 mt-2 py-2">
|
|
533
|
+
<i class="bi bi-link-45deg me-1"></i>{antenna2Label}
|
|
568
534
|
</div>
|
|
569
535
|
{/if}
|
|
570
536
|
{:else}
|
|
@@ -187,8 +187,7 @@
|
|
|
187
187
|
<thead class="table-light sticky-top">
|
|
188
188
|
<tr>
|
|
189
189
|
<th>Model Name</th>
|
|
190
|
-
<th class="text-center" style="width:
|
|
191
|
-
<th class="text-center" style="width: 100px;">Orig. Freq</th>
|
|
190
|
+
<th class="text-center" style="width: 120px;">Band [Orig]</th>
|
|
192
191
|
<th class="text-center" style="width: 70px;">Tilt</th>
|
|
193
192
|
<th class="text-center" style="width: 80px;">Gain</th>
|
|
194
193
|
<th class="text-center" style="width: 60px;">H-Pts</th>
|
|
@@ -206,9 +205,7 @@
|
|
|
206
205
|
</td>
|
|
207
206
|
<td class="text-center">
|
|
208
207
|
<span class="badge bg-primary">{antenna.frequency}</span>
|
|
209
|
-
|
|
210
|
-
<td class="text-center text-muted">
|
|
211
|
-
{antenna.originalFrequency} MHz
|
|
208
|
+
<span class="text-muted small">[{antenna.originalFrequency}]</span>
|
|
212
209
|
</td>
|
|
213
210
|
<td class="text-center">
|
|
214
211
|
{antenna.tilt}°
|
|
@@ -225,7 +222,7 @@
|
|
|
225
222
|
</tr>
|
|
226
223
|
{:else}
|
|
227
224
|
<tr>
|
|
228
|
-
<td colspan="
|
|
225
|
+
<td colspan="6" class="text-center text-muted py-4">
|
|
229
226
|
No antennas match your search criteria
|
|
230
227
|
</td>
|
|
231
228
|
</tr>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<svelte:options runes={true} />
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
|
-
import {
|
|
4
|
+
import { parseFolderWithErrors, type ParseProgress, type ParseError } from '../utils/msi-parser';
|
|
5
5
|
import { saveAntennasWithPurge, type ImportResult } from '../utils/db-utils';
|
|
6
6
|
import { STANDARD_BANDS } from '../band-config';
|
|
7
7
|
import type { RawAntenna, Antenna, BandDefinition } from '../types';
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
let message = $state('');
|
|
24
24
|
let importResult = $state<ImportResult | null>(null);
|
|
25
25
|
let parseProgress = $state<ParseProgress | null>(null);
|
|
26
|
+
let parseErrors = $state<ParseError[]>([]);
|
|
27
|
+
let showErrorDetails = $state(false);
|
|
26
28
|
|
|
27
29
|
let recursiveScan = $state(true);
|
|
28
30
|
|
|
@@ -39,6 +41,8 @@
|
|
|
39
41
|
message = 'Selecting folder...';
|
|
40
42
|
importResult = null;
|
|
41
43
|
parseProgress = null;
|
|
44
|
+
parseErrors = [];
|
|
45
|
+
showErrorDetails = false;
|
|
42
46
|
|
|
43
47
|
// Ask user to select a folder
|
|
44
48
|
const directoryPicker = window.showDirectoryPicker as () => Promise<FileSystemDirectoryHandle>;
|
|
@@ -48,13 +52,20 @@
|
|
|
48
52
|
'Parsing files (top-level folder only)...';
|
|
49
53
|
|
|
50
54
|
// Parse all MSI/PNT files in the folder, with recursive option and progress
|
|
51
|
-
|
|
55
|
+
const result = await parseFolderWithErrors(directoryHandle, recursiveScan, (progress) => {
|
|
52
56
|
parseProgress = progress;
|
|
53
57
|
});
|
|
54
58
|
|
|
59
|
+
rawAntennas = result.antennas;
|
|
60
|
+
parseErrors = result.errors;
|
|
61
|
+
|
|
55
62
|
parseProgress = null;
|
|
56
|
-
if (rawAntennas.length === 0) {
|
|
63
|
+
if (rawAntennas.length === 0 && parseErrors.length === 0) {
|
|
57
64
|
message = 'No MSI or PNT files found in the selected folder.';
|
|
65
|
+
} else if (rawAntennas.length === 0 && parseErrors.length > 0) {
|
|
66
|
+
message = `No files were successfully parsed. ${parseErrors.length} file(s) failed.`;
|
|
67
|
+
} else if (parseErrors.length > 0) {
|
|
68
|
+
message = `Parsed ${rawAntennas.length} antenna files${recursiveScan ? ' from all folders' : ''}. ${parseErrors.length} file(s) failed.`;
|
|
58
69
|
} else {
|
|
59
70
|
message = `Successfully parsed ${rawAntennas.length} antenna files${recursiveScan ? ' from all folders' : ''}.`;
|
|
60
71
|
}
|
|
@@ -204,17 +215,21 @@
|
|
|
204
215
|
<span class="text-info fw-semibold">Parsing antenna files...</span>
|
|
205
216
|
</div>
|
|
206
217
|
<div class="row text-center mb-3">
|
|
207
|
-
<div class="col-
|
|
218
|
+
<div class="col-3">
|
|
208
219
|
<div class="h4 mb-0 text-primary">{parseProgress.directoriesScanned}</div>
|
|
209
|
-
<small class="text-muted">Folders
|
|
220
|
+
<small class="text-muted">Folders</small>
|
|
210
221
|
</div>
|
|
211
|
-
<div class="col-
|
|
222
|
+
<div class="col-3">
|
|
212
223
|
<div class="h4 mb-0 text-info">{parseProgress.filesProcessed}</div>
|
|
213
|
-
<small class="text-muted">
|
|
224
|
+
<small class="text-muted">Processed</small>
|
|
214
225
|
</div>
|
|
215
|
-
<div class="col-
|
|
226
|
+
<div class="col-3">
|
|
216
227
|
<div class="h4 mb-0 text-success">{parseProgress.antennaFilesFound}</div>
|
|
217
|
-
<small class="text-muted">
|
|
228
|
+
<small class="text-muted">Parsed</small>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="col-3">
|
|
231
|
+
<div class="h4 mb-0 {parseProgress.failedFiles > 0 ? 'text-danger' : 'text-muted'}">{parseProgress.failedFiles}</div>
|
|
232
|
+
<small class="text-muted">Failed</small>
|
|
218
233
|
</div>
|
|
219
234
|
</div>
|
|
220
235
|
<div class="text-muted small text-truncate">
|
|
@@ -232,6 +247,57 @@
|
|
|
232
247
|
</div>
|
|
233
248
|
{/if}
|
|
234
249
|
|
|
250
|
+
<!-- Parse Errors Summary -->
|
|
251
|
+
{#if parseErrors.length > 0 && !parseProgress}
|
|
252
|
+
<div class="alert alert-warning border-0 bg-warning-subtle" role="alert">
|
|
253
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
254
|
+
<div>
|
|
255
|
+
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
256
|
+
<strong>{parseErrors.length} file(s) failed to parse</strong>
|
|
257
|
+
<span class="text-muted ms-2">- Import continued with remaining files</span>
|
|
258
|
+
</div>
|
|
259
|
+
<button
|
|
260
|
+
class="btn btn-sm btn-outline-warning"
|
|
261
|
+
onclick={() => showErrorDetails = !showErrorDetails}
|
|
262
|
+
>
|
|
263
|
+
{showErrorDetails ? 'Hide' : 'Show'} details
|
|
264
|
+
</button>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{#if showErrorDetails}
|
|
268
|
+
<hr class="my-2">
|
|
269
|
+
<div class="small" style="max-height: 200px; overflow-y: auto;">
|
|
270
|
+
<table class="table table-sm table-borderless mb-0">
|
|
271
|
+
<thead>
|
|
272
|
+
<tr>
|
|
273
|
+
<th style="width: 60px;">Type</th>
|
|
274
|
+
<th>Path</th>
|
|
275
|
+
<th>Error</th>
|
|
276
|
+
</tr>
|
|
277
|
+
</thead>
|
|
278
|
+
<tbody>
|
|
279
|
+
{#each parseErrors as parseError}
|
|
280
|
+
<tr>
|
|
281
|
+
<td>
|
|
282
|
+
{#if parseError.type === 'folder'}
|
|
283
|
+
<span class="badge bg-secondary"><i class="bi bi-folder"></i></span>
|
|
284
|
+
{:else}
|
|
285
|
+
<span class="badge bg-danger"><i class="bi bi-file-earmark"></i></span>
|
|
286
|
+
{/if}
|
|
287
|
+
</td>
|
|
288
|
+
<td class="text-truncate" style="max-width: 300px;" title={parseError.path}>
|
|
289
|
+
{parseError.path}
|
|
290
|
+
</td>
|
|
291
|
+
<td class="text-muted">{parseError.message}</td>
|
|
292
|
+
</tr>
|
|
293
|
+
{/each}
|
|
294
|
+
</tbody>
|
|
295
|
+
</table>
|
|
296
|
+
</div>
|
|
297
|
+
{/if}
|
|
298
|
+
</div>
|
|
299
|
+
{/if}
|
|
300
|
+
|
|
235
301
|
{#if error}
|
|
236
302
|
<div class="alert alert-danger border-0 bg-danger-subtle text-danger-emphasis" role="alert">
|
|
237
303
|
<i class="bi bi-exclamation-octagon me-2"></i>
|
|
@@ -24,12 +24,46 @@ export interface ParseProgress {
|
|
|
24
24
|
directoriesScanned: number;
|
|
25
25
|
/** Number of MSI/PNT files found */
|
|
26
26
|
antennaFilesFound: number;
|
|
27
|
+
/** Number of files that failed to parse */
|
|
28
|
+
failedFiles: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Error information for failed file/folder parsing
|
|
32
|
+
*/
|
|
33
|
+
export interface ParseError {
|
|
34
|
+
/** Path to the file or folder that failed */
|
|
35
|
+
path: string;
|
|
36
|
+
/** Error message */
|
|
37
|
+
message: string;
|
|
38
|
+
/** Type of error */
|
|
39
|
+
type: 'file' | 'folder';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Result of folder parsing including errors
|
|
43
|
+
*/
|
|
44
|
+
export interface ParseFolderResult {
|
|
45
|
+
/** Successfully parsed antennas */
|
|
46
|
+
antennas: RawAntenna[];
|
|
47
|
+
/** Errors encountered during parsing */
|
|
48
|
+
errors: ParseError[];
|
|
49
|
+
/** Total files attempted */
|
|
50
|
+
totalFilesAttempted: number;
|
|
51
|
+
/** Total directories scanned */
|
|
52
|
+
totalDirectoriesScanned: number;
|
|
27
53
|
}
|
|
28
54
|
/**
|
|
29
55
|
* Parse a folder of MSI files recursively
|
|
30
56
|
* @param directoryHandle - File system directory handle
|
|
31
57
|
* @param recursive - Whether to process subdirectories
|
|
32
58
|
* @param onProgress - Optional callback for progress updates
|
|
33
|
-
* @returns Array of parsed antennas
|
|
59
|
+
* @returns Array of parsed antennas (for backward compatibility)
|
|
34
60
|
*/
|
|
35
61
|
export declare function parseFolder(directoryHandle: FileSystemDirectoryHandle, recursive?: boolean, onProgress?: (progress: ParseProgress) => void): Promise<RawAntenna[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Parse a folder of MSI files recursively with detailed error reporting
|
|
64
|
+
* @param directoryHandle - File system directory handle
|
|
65
|
+
* @param recursive - Whether to process subdirectories
|
|
66
|
+
* @param onProgress - Optional callback for progress updates
|
|
67
|
+
* @returns Object containing parsed antennas and any errors encountered
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseFolderWithErrors(directoryHandle: FileSystemDirectoryHandle, recursive?: boolean, onProgress?: (progress: ParseProgress) => void): Promise<ParseFolderResult>;
|