@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.
Files changed (36) hide show
  1. package/dist/apps/antenna-tools/components/AntennaControls.svelte +71 -9
  2. package/dist/apps/antenna-tools/components/AntennaControls.svelte.d.ts +2 -0
  3. package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +4 -174
  4. package/dist/apps/antenna-tools/components/AntennaTools.svelte +48 -82
  5. package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +5 -8
  6. package/dist/apps/antenna-tools/components/MSIConverter.svelte +377 -52
  7. package/dist/apps/antenna-tools/db.js +4 -0
  8. package/dist/apps/antenna-tools/utils/db-utils.d.ts +19 -0
  9. package/dist/apps/antenna-tools/utils/db-utils.js +108 -0
  10. package/dist/apps/antenna-tools/utils/msi-parser.d.ts +35 -1
  11. package/dist/apps/antenna-tools/utils/msi-parser.js +105 -35
  12. package/dist/core/Auth/LoginForm.svelte +397 -0
  13. package/dist/core/Auth/LoginForm.svelte.d.ts +16 -0
  14. package/dist/core/Auth/auth.svelte.d.ts +22 -0
  15. package/dist/core/Auth/auth.svelte.js +184 -0
  16. package/dist/core/Auth/config.d.ts +25 -0
  17. package/dist/core/Auth/config.js +256 -0
  18. package/dist/core/Auth/index.d.ts +4 -0
  19. package/dist/core/Auth/index.js +5 -0
  20. package/dist/core/Auth/types.d.ts +140 -0
  21. package/dist/core/Auth/types.js +2 -0
  22. package/dist/core/Benchmark/Benchmark.svelte +662 -0
  23. package/dist/core/Benchmark/Benchmark.svelte.d.ts +3 -0
  24. package/dist/core/Benchmark/benchmark-utils.d.ts +48 -0
  25. package/dist/core/Benchmark/benchmark-utils.js +80 -0
  26. package/dist/core/Benchmark/index.d.ts +2 -0
  27. package/dist/core/Benchmark/index.js +3 -0
  28. package/dist/core/LandingPage/App.svelte +102 -0
  29. package/dist/core/LandingPage/App.svelte.d.ts +20 -0
  30. package/dist/core/LandingPage/LandingPage.svelte +480 -0
  31. package/dist/core/LandingPage/LandingPage.svelte.d.ts +21 -0
  32. package/dist/core/LandingPage/index.d.ts +2 -0
  33. package/dist/core/LandingPage/index.js +3 -0
  34. package/dist/core/index.d.ts +3 -0
  35. package/dist/core/index.js +6 -0
  36. package/package.json +1 -1
@@ -1,9 +1,21 @@
1
1
  <svelte:options runes={true} />
2
2
 
3
3
  <script lang="ts">
4
- import { parseFolder, type ParseProgress } from '../utils/msi-parser';
5
- import { saveAntennasWithPurge, type ImportResult } from '../utils/db-utils';
6
- import { STANDARD_BANDS } from '../band-config';
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
- rawAntennas = await parseFolder(directoryHandle, recursiveScan, (progress) => {
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
- // Save with automatic purge (using custom bands if provided)
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 = `Successfully imported ${stats.totalAfter} antennas (${stats.reductionPercent}% reduction from ${stats.totalBefore} raw files).`;
168
+ message = `Replaced database with ${stats.totalAfter} antennas (${stats.reductionPercent}% reduction from ${stats.totalBefore} raw files).`;
94
169
 
95
- // Notify parent of data refresh
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">Convert MSI / PNT to JSON</h1>
135
- <p class="text-muted mb-0">Scan a folder of antenna definitions. Files will be purged to keep only configured bands.</p>
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
- <div class="text-center">
153
- <button class="btn btn-primary btn-lg px-4" onclick={selectFolder} disabled={isLoading}>
154
- {#if isLoading}
155
- <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
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
- </div>
173
-
174
- {#if rawAntennas.length > 0}
175
- <div class="row g-3">
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
- Saving
347
+ Processing
181
348
  {:else}
182
- <i class="bi bi-database-add me-2"></i>
183
- Import to database (with purge)
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
- <div class="col-md-6">
188
- <button class="btn btn-outline-secondary w-100" onclick={downloadJson}>
189
- <i class="bi bi-download me-2"></i>
190
- Download raw JSON
191
- </button>
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
- {/if}
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-4">
477
+ <div class="col-3">
208
478
  <div class="h4 mb-0 text-primary">{parseProgress.directoriesScanned}</div>
209
- <small class="text-muted">Folders scanned</small>
479
+ <small class="text-muted">Folders</small>
210
480
  </div>
211
- <div class="col-4">
481
+ <div class="col-3">
212
482
  <div class="h4 mb-0 text-info">{parseProgress.filesProcessed}</div>
213
- <small class="text-muted">Files processed</small>
483
+ <small class="text-muted">Processed</small>
214
484
  </div>
215
- <div class="col-4">
485
+ <div class="col-3">
216
486
  <div class="h4 mb-0 text-success">{parseProgress.antennaFilesFound}</div>
217
- <small class="text-muted">Antennas found</small>
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
  // ─────────────────────────────────────────────────────────────────────────────