@smartnet360/svelte-components 0.0.123 → 0.0.125
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/AntennaSettingsModal.svelte +4 -174
- package/dist/apps/antenna-tools/components/AntennaTools.svelte +48 -82
- package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +5 -8
- package/dist/apps/antenna-tools/components/MSIConverter.svelte +377 -52
- package/dist/apps/antenna-tools/db.js +4 -0
- package/dist/apps/antenna-tools/utils/db-utils.d.ts +19 -0
- package/dist/apps/antenna-tools/utils/db-utils.js +108 -0
- 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/Auth/LoginForm.svelte +397 -0
- package/dist/core/Auth/LoginForm.svelte.d.ts +16 -0
- package/dist/core/Auth/auth.svelte.d.ts +22 -0
- package/dist/core/Auth/auth.svelte.js +184 -0
- package/dist/core/Auth/config.d.ts +25 -0
- package/dist/core/Auth/config.js +256 -0
- package/dist/core/Auth/index.d.ts +4 -0
- package/dist/core/Auth/index.js +5 -0
- package/dist/core/Auth/types.d.ts +140 -0
- package/dist/core/Auth/types.js +2 -0
- 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/LandingPage/App.svelte +102 -0
- package/dist/core/LandingPage/App.svelte.d.ts +20 -0
- package/dist/core/LandingPage/LandingPage.svelte +480 -0
- package/dist/core/LandingPage/LandingPage.svelte.d.ts +21 -0
- package/dist/core/LandingPage/index.d.ts +2 -0
- package/dist/core/LandingPage/index.js +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +6 -0
- package/package.json +1 -1
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
<svelte:options runes={true} />
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { untrack } from 'svelte';
|
|
5
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
6
|
+
import { parseFolderWithErrors, type ParseProgress, type ParseError } from '../utils/msi-parser';
|
|
7
|
+
import {
|
|
8
|
+
saveAntennasWithPurge,
|
|
9
|
+
addAntennas,
|
|
10
|
+
clearAllAntennas,
|
|
11
|
+
clearAntennasByBands,
|
|
12
|
+
getAntennasCountByBand,
|
|
13
|
+
exportAntennas,
|
|
14
|
+
loadAntennas,
|
|
15
|
+
type ImportResult
|
|
16
|
+
} from '../utils/db-utils';
|
|
17
|
+
import { purgeAntennas, calculatePurgeStats } from '../band-config';
|
|
18
|
+
import { STANDARD_BANDS } from '../types';
|
|
7
19
|
import type { RawAntenna, Antenna, BandDefinition } from '../types';
|
|
8
20
|
|
|
9
21
|
interface Props {
|
|
@@ -23,9 +35,28 @@
|
|
|
23
35
|
let message = $state('');
|
|
24
36
|
let importResult = $state<ImportResult | null>(null);
|
|
25
37
|
let parseProgress = $state<ParseProgress | null>(null);
|
|
38
|
+
let parseErrors = $state<ParseError[]>([]);
|
|
39
|
+
let showErrorDetails = $state(false);
|
|
40
|
+
|
|
41
|
+
// Database management state
|
|
42
|
+
let bandCounts = $state<Map<number, number>>(new Map());
|
|
43
|
+
let selectedBandsForPurge = new SvelteSet<number>();
|
|
44
|
+
let showPurgeByBand = $state(false);
|
|
26
45
|
|
|
27
46
|
let recursiveScan = $state(true);
|
|
28
47
|
|
|
48
|
+
// Load band counts on mount
|
|
49
|
+
async function refreshBandCounts() {
|
|
50
|
+
bandCounts = await getAntennasCountByBand();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Refresh band counts when component mounts (run once)
|
|
54
|
+
$effect(() => {
|
|
55
|
+
untrack(() => {
|
|
56
|
+
refreshBandCounts();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
29
60
|
async function selectFolder() {
|
|
30
61
|
try {
|
|
31
62
|
// Check if File System Access API is supported
|
|
@@ -39,6 +70,8 @@
|
|
|
39
70
|
message = 'Selecting folder...';
|
|
40
71
|
importResult = null;
|
|
41
72
|
parseProgress = null;
|
|
73
|
+
parseErrors = [];
|
|
74
|
+
showErrorDetails = false;
|
|
42
75
|
|
|
43
76
|
// Ask user to select a folder
|
|
44
77
|
const directoryPicker = window.showDirectoryPicker as () => Promise<FileSystemDirectoryHandle>;
|
|
@@ -48,13 +81,20 @@
|
|
|
48
81
|
'Parsing files (top-level folder only)...';
|
|
49
82
|
|
|
50
83
|
// Parse all MSI/PNT files in the folder, with recursive option and progress
|
|
51
|
-
|
|
84
|
+
const result = await parseFolderWithErrors(directoryHandle, recursiveScan, (progress) => {
|
|
52
85
|
parseProgress = progress;
|
|
53
86
|
});
|
|
54
87
|
|
|
88
|
+
rawAntennas = result.antennas;
|
|
89
|
+
parseErrors = result.errors;
|
|
90
|
+
|
|
55
91
|
parseProgress = null;
|
|
56
|
-
if (rawAntennas.length === 0) {
|
|
92
|
+
if (rawAntennas.length === 0 && parseErrors.length === 0) {
|
|
57
93
|
message = 'No MSI or PNT files found in the selected folder.';
|
|
94
|
+
} else if (rawAntennas.length === 0 && parseErrors.length > 0) {
|
|
95
|
+
message = `No files were successfully parsed. ${parseErrors.length} file(s) failed.`;
|
|
96
|
+
} else if (parseErrors.length > 0) {
|
|
97
|
+
message = `Parsed ${rawAntennas.length} antenna files${recursiveScan ? ' from all folders' : ''}. ${parseErrors.length} file(s) failed.`;
|
|
58
98
|
} else {
|
|
59
99
|
message = `Successfully parsed ${rawAntennas.length} antenna files${recursiveScan ? ' from all folders' : ''}.`;
|
|
60
100
|
}
|
|
@@ -85,14 +125,50 @@
|
|
|
85
125
|
isLoading = true;
|
|
86
126
|
message = 'Purging frequencies and saving to database...';
|
|
87
127
|
|
|
88
|
-
//
|
|
128
|
+
// Purge first
|
|
129
|
+
const purgedAntennas = purgeAntennas(rawAntennas, customBands);
|
|
130
|
+
const purgeStats = calculatePurgeStats(rawAntennas.length, purgedAntennas);
|
|
131
|
+
|
|
132
|
+
// Add to database (merge mode - won't clear existing)
|
|
133
|
+
const { added, updated } = await addAntennas(purgedAntennas);
|
|
134
|
+
|
|
135
|
+
importResult = {
|
|
136
|
+
success: true,
|
|
137
|
+
purgeStats
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
message = `Imported ${purgeStats.totalAfter} antennas (${added} new, ${updated} updated). ${purgeStats.reductionPercent}% reduction from ${purgeStats.totalBefore} raw files.`;
|
|
141
|
+
|
|
142
|
+
// Refresh band counts and notify parent
|
|
143
|
+
await refreshBandCounts();
|
|
144
|
+
onDataRefresh?.();
|
|
145
|
+
|
|
146
|
+
} catch (e) {
|
|
147
|
+
error = e instanceof Error ? e.message : 'Failed to save antennas to database';
|
|
148
|
+
} finally {
|
|
149
|
+
isLoading = false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function replaceDatabase() {
|
|
154
|
+
if (rawAntennas.length === 0) {
|
|
155
|
+
error = 'No antenna data to save.';
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
isLoading = true;
|
|
161
|
+
message = 'Replacing database with new data...';
|
|
162
|
+
|
|
163
|
+
// Save with purge (clears existing first)
|
|
89
164
|
importResult = await saveAntennasWithPurge(rawAntennas, customBands);
|
|
90
165
|
|
|
91
166
|
if (importResult.success) {
|
|
92
167
|
const stats = importResult.purgeStats;
|
|
93
|
-
message = `
|
|
168
|
+
message = `Replaced database with ${stats.totalAfter} antennas (${stats.reductionPercent}% reduction from ${stats.totalBefore} raw files).`;
|
|
94
169
|
|
|
95
|
-
//
|
|
170
|
+
// Refresh band counts and notify parent
|
|
171
|
+
await refreshBandCounts();
|
|
96
172
|
onDataRefresh?.();
|
|
97
173
|
} else {
|
|
98
174
|
error = importResult.error || 'Failed to save antennas to database';
|
|
@@ -125,16 +201,125 @@
|
|
|
125
201
|
|
|
126
202
|
message = 'JSON file downloaded (raw data, not purged).';
|
|
127
203
|
}
|
|
204
|
+
|
|
205
|
+
async function handleExportDatabase() {
|
|
206
|
+
try {
|
|
207
|
+
isLoading = true;
|
|
208
|
+
message = 'Exporting database...';
|
|
209
|
+
await exportAntennas();
|
|
210
|
+
message = 'Database exported successfully.';
|
|
211
|
+
} catch (e) {
|
|
212
|
+
error = e instanceof Error ? e.message : 'Failed to export database';
|
|
213
|
+
} finally {
|
|
214
|
+
isLoading = false;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function handleImportJson(event: Event) {
|
|
219
|
+
const input = event.target as HTMLInputElement;
|
|
220
|
+
const file = input.files?.[0];
|
|
221
|
+
if (!file) return;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
isLoading = true;
|
|
225
|
+
message = 'Importing JSON file...';
|
|
226
|
+
|
|
227
|
+
const text = await file.text();
|
|
228
|
+
const data = JSON.parse(text) as Antenna[];
|
|
229
|
+
|
|
230
|
+
// Add to database (merge mode)
|
|
231
|
+
const { added, updated } = await addAntennas(data);
|
|
232
|
+
|
|
233
|
+
message = `Imported ${data.length} antennas (${added} new, ${updated} updated).`;
|
|
234
|
+
|
|
235
|
+
// Refresh band counts and notify parent
|
|
236
|
+
await refreshBandCounts();
|
|
237
|
+
onDataRefresh?.();
|
|
238
|
+
|
|
239
|
+
// Clear file input
|
|
240
|
+
input.value = '';
|
|
241
|
+
} catch (e) {
|
|
242
|
+
error = e instanceof Error ? e.message : 'Failed to import JSON file';
|
|
243
|
+
} finally {
|
|
244
|
+
isLoading = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function handlePurgeAll() {
|
|
249
|
+
if (!confirm('Are you sure you want to delete ALL antennas from the database?')) return;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
isLoading = true;
|
|
253
|
+
message = 'Clearing database...';
|
|
254
|
+
await clearAllAntennas();
|
|
255
|
+
message = 'Database cleared successfully.';
|
|
256
|
+
importResult = null;
|
|
257
|
+
rawAntennas = [];
|
|
258
|
+
|
|
259
|
+
// Refresh band counts and notify parent
|
|
260
|
+
await refreshBandCounts();
|
|
261
|
+
onDataRefresh?.();
|
|
262
|
+
} catch (e) {
|
|
263
|
+
error = e instanceof Error ? e.message : 'Failed to clear database';
|
|
264
|
+
} finally {
|
|
265
|
+
isLoading = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function handlePurgeByBands() {
|
|
270
|
+
if (selectedBandsForPurge.size === 0) {
|
|
271
|
+
error = 'Please select at least one band to purge.';
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const bandsArray = Array.from(selectedBandsForPurge);
|
|
276
|
+
if (!confirm(`Are you sure you want to delete all antennas in bands: ${bandsArray.join(', ')} MHz?`)) return;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
isLoading = true;
|
|
280
|
+
message = `Purging bands: ${bandsArray.join(', ')} MHz...`;
|
|
281
|
+
const deleted = await clearAntennasByBands(bandsArray);
|
|
282
|
+
message = `Deleted ${deleted} antennas from bands: ${bandsArray.join(', ')} MHz.`;
|
|
283
|
+
selectedBandsForPurge.clear();
|
|
284
|
+
showPurgeByBand = false;
|
|
285
|
+
|
|
286
|
+
// Refresh band counts and notify parent
|
|
287
|
+
await refreshBandCounts();
|
|
288
|
+
onDataRefresh?.();
|
|
289
|
+
} catch (e) {
|
|
290
|
+
error = e instanceof Error ? e.message : 'Failed to purge bands';
|
|
291
|
+
} finally {
|
|
292
|
+
isLoading = false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function toggleBandSelection(band: number) {
|
|
297
|
+
if (selectedBandsForPurge.has(band)) {
|
|
298
|
+
selectedBandsForPurge.delete(band);
|
|
299
|
+
} else {
|
|
300
|
+
selectedBandsForPurge.add(band);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Calculate total antennas in database
|
|
305
|
+
let totalInDatabase = $derived(Array.from(bandCounts.values()).reduce((a, b) => a + b, 0));
|
|
128
306
|
</script>
|
|
129
307
|
|
|
130
308
|
<div class="card border-0 shadow-sm">
|
|
131
309
|
<div class="card-body">
|
|
132
310
|
<header class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-4">
|
|
133
311
|
<div>
|
|
134
|
-
<h1 class="h3 mb-2">
|
|
135
|
-
<p class="text-muted mb-0">
|
|
312
|
+
<h1 class="h3 mb-2">Antenna Database Management</h1>
|
|
313
|
+
<p class="text-muted mb-0">Import MSI/PNT files, export data, and manage frequency bands.</p>
|
|
314
|
+
</div>
|
|
315
|
+
<div class="d-flex align-items-center gap-2">
|
|
316
|
+
{#if totalInDatabase > 0}
|
|
317
|
+
<span class="badge text-bg-primary fs-6">{totalInDatabase} antennas</span>
|
|
318
|
+
{:else}
|
|
319
|
+
<span class="badge text-bg-secondary">Empty database</span>
|
|
320
|
+
{/if}
|
|
321
|
+
<span class="badge text-bg-success">Local processing</span>
|
|
136
322
|
</div>
|
|
137
|
-
<span class="badge text-bg-success">Local processing</span>
|
|
138
323
|
</header>
|
|
139
324
|
|
|
140
325
|
<!-- Bands Info -->
|
|
@@ -149,49 +334,134 @@
|
|
|
149
334
|
</div>
|
|
150
335
|
|
|
151
336
|
<div class="d-flex flex-column gap-4">
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
Processing…
|
|
157
|
-
{:else}
|
|
158
|
-
<i class="bi bi-folder-plus me-2"></i>
|
|
159
|
-
Choose folder with MSI / PNT files
|
|
160
|
-
{/if}
|
|
161
|
-
</button>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<div class="d-flex justify-content-center">
|
|
165
|
-
<div class="form-check form-switch">
|
|
166
|
-
<input class="form-check-input" type="checkbox" bind:checked={recursiveScan} id="recursiveScan" />
|
|
167
|
-
<label class="form-check-label" for="recursiveScan">
|
|
168
|
-
<i class="bi bi-arrow-down-circle me-1"></i>
|
|
169
|
-
Include sub-folders during scan
|
|
170
|
-
</label>
|
|
337
|
+
<!-- Import Section -->
|
|
338
|
+
<div class="card border">
|
|
339
|
+
<div class="card-header bg-light">
|
|
340
|
+
<i class="bi bi-folder-plus me-2"></i>Import from MSI/PNT Folder
|
|
171
341
|
</div>
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<div class="col-md-6">
|
|
177
|
-
<button class="btn btn-success w-100" onclick={saveToDatabase} disabled={isLoading}>
|
|
178
|
-
{#if isLoading}
|
|
342
|
+
<div class="card-body">
|
|
343
|
+
<div class="text-center mb-3">
|
|
344
|
+
<button class="btn btn-primary btn-lg px-4" onclick={selectFolder} disabled={isLoading}>
|
|
345
|
+
{#if isLoading && parseProgress}
|
|
179
346
|
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
|
180
|
-
|
|
347
|
+
Processing…
|
|
181
348
|
{:else}
|
|
182
|
-
<i class="bi bi-
|
|
183
|
-
|
|
349
|
+
<i class="bi bi-folder-plus me-2"></i>
|
|
350
|
+
Choose folder with MSI / PNT files
|
|
184
351
|
{/if}
|
|
185
352
|
</button>
|
|
186
353
|
</div>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
354
|
+
|
|
355
|
+
<div class="d-flex justify-content-center">
|
|
356
|
+
<div class="form-check form-switch">
|
|
357
|
+
<input class="form-check-input" type="checkbox" bind:checked={recursiveScan} id="recursiveScan" />
|
|
358
|
+
<label class="form-check-label" for="recursiveScan">
|
|
359
|
+
<i class="bi bi-arrow-down-circle me-1"></i>
|
|
360
|
+
Include sub-folders during scan
|
|
361
|
+
</label>
|
|
362
|
+
</div>
|
|
192
363
|
</div>
|
|
364
|
+
|
|
365
|
+
{#if rawAntennas.length > 0}
|
|
366
|
+
<hr>
|
|
367
|
+
<div class="row g-3">
|
|
368
|
+
<div class="col-md-4">
|
|
369
|
+
<button class="btn btn-success w-100" onclick={saveToDatabase} disabled={isLoading}>
|
|
370
|
+
<i class="bi bi-database-add me-2"></i>
|
|
371
|
+
Add to database
|
|
372
|
+
</button>
|
|
373
|
+
<small class="text-muted d-block text-center mt-1">Merge with existing</small>
|
|
374
|
+
</div>
|
|
375
|
+
<div class="col-md-4">
|
|
376
|
+
<button class="btn btn-warning w-100" onclick={replaceDatabase} disabled={isLoading}>
|
|
377
|
+
<i class="bi bi-arrow-repeat me-2"></i>
|
|
378
|
+
Replace database
|
|
379
|
+
</button>
|
|
380
|
+
<small class="text-muted d-block text-center mt-1">Clear first, then add</small>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="col-md-4">
|
|
383
|
+
<button class="btn btn-outline-secondary w-100" onclick={downloadJson}>
|
|
384
|
+
<i class="bi bi-download me-2"></i>
|
|
385
|
+
Download raw JSON
|
|
386
|
+
</button>
|
|
387
|
+
<small class="text-muted d-block text-center mt-1">Before purge</small>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
{/if}
|
|
193
391
|
</div>
|
|
194
|
-
|
|
392
|
+
</div>
|
|
393
|
+
|
|
394
|
+
<!-- Database Management Section -->
|
|
395
|
+
<div class="card border">
|
|
396
|
+
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
397
|
+
<span><i class="bi bi-database me-2"></i>Database Management</span>
|
|
398
|
+
</div>
|
|
399
|
+
<div class="card-body">
|
|
400
|
+
<div class="row g-3 mb-3">
|
|
401
|
+
<div class="col-md-4">
|
|
402
|
+
<button class="btn btn-outline-primary w-100" onclick={handleExportDatabase} disabled={isLoading || totalInDatabase === 0}>
|
|
403
|
+
<i class="bi bi-box-arrow-up me-2"></i>
|
|
404
|
+
Export to JSON
|
|
405
|
+
</button>
|
|
406
|
+
</div>
|
|
407
|
+
<div class="col-md-4">
|
|
408
|
+
<label class="btn btn-outline-primary w-100 mb-0" style="cursor: pointer;">
|
|
409
|
+
<i class="bi bi-box-arrow-in-down me-2"></i>
|
|
410
|
+
Import from JSON
|
|
411
|
+
<input type="file" accept=".json" onchange={handleImportJson} class="d-none" disabled={isLoading} />
|
|
412
|
+
</label>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="col-md-4">
|
|
415
|
+
<button class="btn btn-outline-danger w-100" onclick={handlePurgeAll} disabled={isLoading || totalInDatabase === 0}>
|
|
416
|
+
<i class="bi bi-trash me-2"></i>
|
|
417
|
+
Purge All
|
|
418
|
+
</button>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<!-- Band-specific purge -->
|
|
423
|
+
{#if bandCounts.size > 0}
|
|
424
|
+
<div class="border rounded p-3 bg-light">
|
|
425
|
+
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
426
|
+
<strong><i class="bi bi-layers me-2"></i>Antennas by Band</strong>
|
|
427
|
+
<button
|
|
428
|
+
class="btn btn-sm {showPurgeByBand ? 'btn-secondary' : 'btn-outline-secondary'}"
|
|
429
|
+
onclick={() => showPurgeByBand = !showPurgeByBand}
|
|
430
|
+
>
|
|
431
|
+
{showPurgeByBand ? 'Cancel' : 'Purge by Band'}
|
|
432
|
+
</button>
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<div class="d-flex flex-wrap gap-2">
|
|
436
|
+
{#each Array.from(bandCounts.entries()).sort((a, b) => a[0] - b[0]) as [band, count]}
|
|
437
|
+
{#if showPurgeByBand}
|
|
438
|
+
<button
|
|
439
|
+
class="btn btn-sm {selectedBandsForPurge.has(band) ? 'btn-danger' : 'btn-outline-secondary'}"
|
|
440
|
+
onclick={() => toggleBandSelection(band)}
|
|
441
|
+
>
|
|
442
|
+
{band} MHz ({count})
|
|
443
|
+
{#if selectedBandsForPurge.has(band)}
|
|
444
|
+
<i class="bi bi-x-circle ms-1"></i>
|
|
445
|
+
{/if}
|
|
446
|
+
</button>
|
|
447
|
+
{:else}
|
|
448
|
+
<span class="badge text-bg-secondary">{band} MHz: {count}</span>
|
|
449
|
+
{/if}
|
|
450
|
+
{/each}
|
|
451
|
+
</div>
|
|
452
|
+
|
|
453
|
+
{#if showPurgeByBand && selectedBandsForPurge.size > 0}
|
|
454
|
+
<div class="mt-3">
|
|
455
|
+
<button class="btn btn-danger" onclick={handlePurgeByBands} disabled={isLoading}>
|
|
456
|
+
<i class="bi bi-trash me-2"></i>
|
|
457
|
+
Delete {selectedBandsForPurge.size} band(s)
|
|
458
|
+
</button>
|
|
459
|
+
</div>
|
|
460
|
+
{/if}
|
|
461
|
+
</div>
|
|
462
|
+
{/if}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
195
465
|
|
|
196
466
|
<!-- Progress indicator during parsing -->
|
|
197
467
|
{#if isLoading && parseProgress}
|
|
@@ -204,17 +474,21 @@
|
|
|
204
474
|
<span class="text-info fw-semibold">Parsing antenna files...</span>
|
|
205
475
|
</div>
|
|
206
476
|
<div class="row text-center mb-3">
|
|
207
|
-
<div class="col-
|
|
477
|
+
<div class="col-3">
|
|
208
478
|
<div class="h4 mb-0 text-primary">{parseProgress.directoriesScanned}</div>
|
|
209
|
-
<small class="text-muted">Folders
|
|
479
|
+
<small class="text-muted">Folders</small>
|
|
210
480
|
</div>
|
|
211
|
-
<div class="col-
|
|
481
|
+
<div class="col-3">
|
|
212
482
|
<div class="h4 mb-0 text-info">{parseProgress.filesProcessed}</div>
|
|
213
|
-
<small class="text-muted">
|
|
483
|
+
<small class="text-muted">Processed</small>
|
|
214
484
|
</div>
|
|
215
|
-
<div class="col-
|
|
485
|
+
<div class="col-3">
|
|
216
486
|
<div class="h4 mb-0 text-success">{parseProgress.antennaFilesFound}</div>
|
|
217
|
-
<small class="text-muted">
|
|
487
|
+
<small class="text-muted">Parsed</small>
|
|
488
|
+
</div>
|
|
489
|
+
<div class="col-3">
|
|
490
|
+
<div class="h4 mb-0 {parseProgress.failedFiles > 0 ? 'text-danger' : 'text-muted'}">{parseProgress.failedFiles}</div>
|
|
491
|
+
<small class="text-muted">Failed</small>
|
|
218
492
|
</div>
|
|
219
493
|
</div>
|
|
220
494
|
<div class="text-muted small text-truncate">
|
|
@@ -232,6 +506,57 @@
|
|
|
232
506
|
</div>
|
|
233
507
|
{/if}
|
|
234
508
|
|
|
509
|
+
<!-- Parse Errors Summary -->
|
|
510
|
+
{#if parseErrors.length > 0 && !parseProgress}
|
|
511
|
+
<div class="alert alert-warning border-0 bg-warning-subtle" role="alert">
|
|
512
|
+
<div class="d-flex justify-content-between align-items-center">
|
|
513
|
+
<div>
|
|
514
|
+
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
515
|
+
<strong>{parseErrors.length} file(s) failed to parse</strong>
|
|
516
|
+
<span class="text-muted ms-2">- Import continued with remaining files</span>
|
|
517
|
+
</div>
|
|
518
|
+
<button
|
|
519
|
+
class="btn btn-sm btn-outline-warning"
|
|
520
|
+
onclick={() => showErrorDetails = !showErrorDetails}
|
|
521
|
+
>
|
|
522
|
+
{showErrorDetails ? 'Hide' : 'Show'} details
|
|
523
|
+
</button>
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
{#if showErrorDetails}
|
|
527
|
+
<hr class="my-2">
|
|
528
|
+
<div class="small" style="max-height: 200px; overflow-y: auto;">
|
|
529
|
+
<table class="table table-sm table-borderless mb-0">
|
|
530
|
+
<thead>
|
|
531
|
+
<tr>
|
|
532
|
+
<th style="width: 60px;">Type</th>
|
|
533
|
+
<th>Path</th>
|
|
534
|
+
<th>Error</th>
|
|
535
|
+
</tr>
|
|
536
|
+
</thead>
|
|
537
|
+
<tbody>
|
|
538
|
+
{#each parseErrors as parseError}
|
|
539
|
+
<tr>
|
|
540
|
+
<td>
|
|
541
|
+
{#if parseError.type === 'folder'}
|
|
542
|
+
<span class="badge bg-secondary"><i class="bi bi-folder"></i></span>
|
|
543
|
+
{:else}
|
|
544
|
+
<span class="badge bg-danger"><i class="bi bi-file-earmark"></i></span>
|
|
545
|
+
{/if}
|
|
546
|
+
</td>
|
|
547
|
+
<td class="text-truncate" style="max-width: 300px;" title={parseError.path}>
|
|
548
|
+
{parseError.path}
|
|
549
|
+
</td>
|
|
550
|
+
<td class="text-muted">{parseError.message}</td>
|
|
551
|
+
</tr>
|
|
552
|
+
{/each}
|
|
553
|
+
</tbody>
|
|
554
|
+
</table>
|
|
555
|
+
</div>
|
|
556
|
+
{/if}
|
|
557
|
+
</div>
|
|
558
|
+
{/if}
|
|
559
|
+
|
|
235
560
|
{#if error}
|
|
236
561
|
<div class="alert alert-danger border-0 bg-danger-subtle text-danger-emphasis" role="alert">
|
|
237
562
|
<i class="bi bi-exclamation-octagon me-2"></i>
|
|
@@ -11,6 +11,10 @@ class AntennaToolsDatabase extends Dexie {
|
|
|
11
11
|
this.version(1).stores({
|
|
12
12
|
antennas: '++id, name, frequency, originalFrequency, tilt'
|
|
13
13
|
});
|
|
14
|
+
// Version 2: Add compound index for duplicate detection
|
|
15
|
+
this.version(2).stores({
|
|
16
|
+
antennas: '++id, name, frequency, originalFrequency, tilt, [name+frequency+tilt]'
|
|
17
|
+
});
|
|
14
18
|
}
|
|
15
19
|
/**
|
|
16
20
|
* Check if database has any antenna data
|
|
@@ -45,6 +45,25 @@ export declare function exportAntennas(): Promise<void>;
|
|
|
45
45
|
* Clear all antenna data from database
|
|
46
46
|
*/
|
|
47
47
|
export declare function clearAllAntennas(): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Clear antennas by specific frequency bands
|
|
50
|
+
* @param bands - Array of band frequencies to delete (e.g., [700, 800, 1800])
|
|
51
|
+
* @returns Number of antennas deleted
|
|
52
|
+
*/
|
|
53
|
+
export declare function clearAntennasByBands(bands: number[]): Promise<number>;
|
|
54
|
+
/**
|
|
55
|
+
* Get count of antennas per frequency band
|
|
56
|
+
* @returns Map of band frequency to count
|
|
57
|
+
*/
|
|
58
|
+
export declare function getAntennasCountByBand(): Promise<Map<number, number>>;
|
|
59
|
+
/**
|
|
60
|
+
* Add antennas to database (merge mode - no clearing)
|
|
61
|
+
* Duplicates (same name + frequency + tilt) are overwritten
|
|
62
|
+
*/
|
|
63
|
+
export declare function addAntennas(newAntennas: Antenna[]): Promise<{
|
|
64
|
+
added: number;
|
|
65
|
+
updated: number;
|
|
66
|
+
}>;
|
|
48
67
|
/**
|
|
49
68
|
* Check if data has been imported before
|
|
50
69
|
*/
|
|
@@ -256,6 +256,114 @@ export async function clearAllAntennas() {
|
|
|
256
256
|
throw error;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Clear antennas by specific frequency bands
|
|
261
|
+
* @param bands - Array of band frequencies to delete (e.g., [700, 800, 1800])
|
|
262
|
+
* @returns Number of antennas deleted
|
|
263
|
+
*/
|
|
264
|
+
export async function clearAntennasByBands(bands) {
|
|
265
|
+
try {
|
|
266
|
+
trackDataOperation('clear', {
|
|
267
|
+
inProgress: true,
|
|
268
|
+
message: `Clearing bands: ${bands.join(', ')} MHz...`
|
|
269
|
+
});
|
|
270
|
+
// Get count before deletion
|
|
271
|
+
const toDelete = await db.antennas.where('frequency').anyOf(bands).toArray();
|
|
272
|
+
const deleteCount = toDelete.length;
|
|
273
|
+
// Delete matching antennas
|
|
274
|
+
await db.antennas.where('frequency').anyOf(bands).delete();
|
|
275
|
+
// Reload remaining antennas into store
|
|
276
|
+
const remaining = await db.antennas.toArray();
|
|
277
|
+
antennas.set(remaining);
|
|
278
|
+
updateDbStatus({
|
|
279
|
+
antennaCount: remaining.length,
|
|
280
|
+
lastUpdated: new Date()
|
|
281
|
+
});
|
|
282
|
+
trackDataOperation('clear', {
|
|
283
|
+
inProgress: false,
|
|
284
|
+
success: true,
|
|
285
|
+
message: `Deleted ${deleteCount} antennas from bands: ${bands.join(', ')} MHz`
|
|
286
|
+
});
|
|
287
|
+
return deleteCount;
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
console.error('Error clearing antennas by bands:', error);
|
|
291
|
+
trackDataOperation('clear', {
|
|
292
|
+
inProgress: false,
|
|
293
|
+
success: false,
|
|
294
|
+
error: error instanceof Error ? error.message : 'Unknown error clearing bands',
|
|
295
|
+
message: 'Failed to clear bands'
|
|
296
|
+
});
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get count of antennas per frequency band
|
|
302
|
+
* @returns Map of band frequency to count
|
|
303
|
+
*/
|
|
304
|
+
export async function getAntennasCountByBand() {
|
|
305
|
+
const all = await db.antennas.toArray();
|
|
306
|
+
const counts = new Map();
|
|
307
|
+
for (const antenna of all) {
|
|
308
|
+
const current = counts.get(antenna.frequency) || 0;
|
|
309
|
+
counts.set(antenna.frequency, current + 1);
|
|
310
|
+
}
|
|
311
|
+
return counts;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Add antennas to database (merge mode - no clearing)
|
|
315
|
+
* Duplicates (same name + frequency + tilt) are overwritten
|
|
316
|
+
*/
|
|
317
|
+
export async function addAntennas(newAntennas) {
|
|
318
|
+
try {
|
|
319
|
+
trackDataOperation('import', { inProgress: true, message: 'Adding antennas...' });
|
|
320
|
+
let added = 0;
|
|
321
|
+
let updated = 0;
|
|
322
|
+
for (const antenna of newAntennas) {
|
|
323
|
+
// Check for existing antenna with same name, frequency, and tilt
|
|
324
|
+
const existing = await db.antennas
|
|
325
|
+
.where(['name', 'frequency', 'tilt'])
|
|
326
|
+
.equals([antenna.name, antenna.frequency, antenna.tilt])
|
|
327
|
+
.first();
|
|
328
|
+
if (existing) {
|
|
329
|
+
// Delete and re-add to update (simpler than partial update with arrays)
|
|
330
|
+
await db.antennas.delete(existing.id);
|
|
331
|
+
await db.antennas.add(antenna);
|
|
332
|
+
updated++;
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// Add new
|
|
336
|
+
await db.antennas.add(antenna);
|
|
337
|
+
added++;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Reload store
|
|
341
|
+
const all = await db.antennas.toArray();
|
|
342
|
+
antennas.set(all);
|
|
343
|
+
localStorage.setItem('antenna-tools-data-imported', 'true');
|
|
344
|
+
updateDbStatus({
|
|
345
|
+
initialized: true,
|
|
346
|
+
antennaCount: all.length,
|
|
347
|
+
lastUpdated: new Date()
|
|
348
|
+
});
|
|
349
|
+
trackDataOperation('import', {
|
|
350
|
+
inProgress: false,
|
|
351
|
+
success: true,
|
|
352
|
+
message: `Added ${added}, updated ${updated} antennas`
|
|
353
|
+
});
|
|
354
|
+
return { added, updated };
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('Error adding antennas:', error);
|
|
358
|
+
trackDataOperation('import', {
|
|
359
|
+
inProgress: false,
|
|
360
|
+
success: false,
|
|
361
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
362
|
+
message: 'Add failed'
|
|
363
|
+
});
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
259
367
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
260
368
|
// UTILITY FUNCTIONS
|
|
261
369
|
// ─────────────────────────────────────────────────────────────────────────────
|