@smartnet360/svelte-components 0.0.124 → 0.0.126

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 (53) hide show
  1. package/dist/apps/antenna-tools/components/AntennaSettingsModal.svelte +4 -174
  2. package/dist/apps/antenna-tools/components/DatabaseViewer.svelte +2 -2
  3. package/dist/apps/antenna-tools/components/MSIConverter.svelte +302 -43
  4. package/dist/apps/antenna-tools/db.js +4 -0
  5. package/dist/apps/antenna-tools/utils/db-utils.d.ts +19 -0
  6. package/dist/apps/antenna-tools/utils/db-utils.js +108 -0
  7. package/dist/core/Auth/LoginForm.svelte +397 -0
  8. package/dist/core/Auth/LoginForm.svelte.d.ts +16 -0
  9. package/dist/core/Auth/auth.svelte.d.ts +22 -0
  10. package/dist/core/Auth/auth.svelte.js +229 -0
  11. package/dist/core/Auth/config.d.ts +25 -0
  12. package/dist/core/Auth/config.js +256 -0
  13. package/dist/core/Auth/index.d.ts +4 -0
  14. package/dist/core/Auth/index.js +5 -0
  15. package/dist/core/Auth/types.d.ts +140 -0
  16. package/dist/core/Auth/types.js +2 -0
  17. package/dist/core/LandingPage/App.svelte +102 -0
  18. package/dist/core/LandingPage/App.svelte.d.ts +20 -0
  19. package/dist/core/LandingPage/LandingPage.svelte +480 -0
  20. package/dist/core/LandingPage/LandingPage.svelte.d.ts +21 -0
  21. package/dist/core/LandingPage/index.d.ts +2 -0
  22. package/dist/core/LandingPage/index.js +3 -0
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +4 -0
  25. package/dist/map-v3/demo/DemoMap.svelte +18 -0
  26. package/dist/map-v3/demo/demo-custom-cells.d.ts +21 -0
  27. package/dist/map-v3/demo/demo-custom-cells.js +48 -0
  28. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte +220 -0
  29. package/dist/map-v3/features/cells/custom/components/CustomCellFilterControl.svelte.d.ts +15 -0
  30. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte +306 -0
  31. package/dist/map-v3/features/cells/custom/components/CustomCellSetManager.svelte.d.ts +10 -0
  32. package/dist/map-v3/features/cells/custom/components/index.d.ts +5 -0
  33. package/dist/map-v3/features/cells/custom/components/index.js +5 -0
  34. package/dist/map-v3/features/cells/custom/index.d.ts +32 -0
  35. package/dist/map-v3/features/cells/custom/index.js +35 -0
  36. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte +262 -0
  37. package/dist/map-v3/features/cells/custom/layers/CustomCellsLayer.svelte.d.ts +10 -0
  38. package/dist/map-v3/features/cells/custom/layers/index.d.ts +4 -0
  39. package/dist/map-v3/features/cells/custom/layers/index.js +4 -0
  40. package/dist/map-v3/features/cells/custom/logic/csv-parser.d.ts +20 -0
  41. package/dist/map-v3/features/cells/custom/logic/csv-parser.js +164 -0
  42. package/dist/map-v3/features/cells/custom/logic/index.d.ts +5 -0
  43. package/dist/map-v3/features/cells/custom/logic/index.js +5 -0
  44. package/dist/map-v3/features/cells/custom/logic/tree-adapter.d.ts +24 -0
  45. package/dist/map-v3/features/cells/custom/logic/tree-adapter.js +67 -0
  46. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.d.ts +78 -0
  47. package/dist/map-v3/features/cells/custom/stores/custom-cell-sets.svelte.js +242 -0
  48. package/dist/map-v3/features/cells/custom/stores/index.d.ts +4 -0
  49. package/dist/map-v3/features/cells/custom/stores/index.js +4 -0
  50. package/dist/map-v3/features/cells/custom/types.d.ts +83 -0
  51. package/dist/map-v3/features/cells/custom/types.js +23 -0
  52. package/dist/map-v3/shared/controls/MapControl.svelte +27 -3
  53. package/package.json +1 -1
@@ -1,11 +1,8 @@
1
1
  <svelte:options runes={true} />
2
2
 
3
3
  <script lang="ts">
4
- import JsonImporter from './JsonImporter.svelte';
5
4
  import MSIConverter from './MSIConverter.svelte';
6
- import DbNotification from './DbNotification.svelte';
7
5
  import DatabaseViewer from './DatabaseViewer.svelte';
8
- import { exportAntennas, clearAllAntennas } from '../utils/db-utils';
9
6
  import type { BandDefinition } from '../types';
10
7
 
11
8
  interface Props {
@@ -18,42 +15,12 @@
18
15
 
19
16
  let { show, onClose, onDataRefresh, customBands }: Props = $props();
20
17
 
21
- let activeTab = $state<'view' | 'import' | 'msi' | 'export' | 'manage'>('view');
22
- let showClearConfirm = $state(false);
18
+ let activeTab = $state<'view' | 'manage'>('view');
23
19
 
24
20
  // Handle data refresh after import operations
25
21
  function handleDataRefresh() {
26
22
  onDataRefresh?.();
27
23
  }
28
-
29
- // Export data as JSON
30
- async function handleExport() {
31
- try {
32
- await exportAntennas();
33
- } catch (error) {
34
- console.error('Export failed:', error);
35
- }
36
- }
37
-
38
- // Clear all data with confirmation
39
- async function handleClearData() {
40
- if (!showClearConfirm) {
41
- showClearConfirm = true;
42
- return;
43
- }
44
-
45
- try {
46
- await clearAllAntennas();
47
- showClearConfirm = false;
48
- handleDataRefresh();
49
- } catch (error) {
50
- console.error('Clear data failed:', error);
51
- }
52
- }
53
-
54
- function cancelClear() {
55
- showClearConfirm = false;
56
- }
57
24
  </script>
58
25
 
59
26
  <!-- Modal -->
@@ -91,33 +58,12 @@
91
58
  >
92
59
  <i class="bi bi-table me-2"></i>View Data
93
60
  </button>
94
- <button
95
- class="nav-link {activeTab === 'import' ? 'active' : ''}"
96
- type="button"
97
- onclick={() => activeTab = 'import'}
98
- >
99
- <i class="bi bi-upload me-2"></i>Import JSON
100
- </button>
101
- <button
102
- class="nav-link {activeTab === 'msi' ? 'active' : ''}"
103
- type="button"
104
- onclick={() => activeTab = 'msi'}
105
- >
106
- <i class="bi bi-file-earmark-text me-2"></i>Import MSI
107
- </button>
108
- <button
109
- class="nav-link {activeTab === 'export' ? 'active' : ''}"
110
- type="button"
111
- onclick={() => activeTab = 'export'}
112
- >
113
- <i class="bi bi-download me-2"></i>Export Data
114
- </button>
115
61
  <button
116
62
  class="nav-link {activeTab === 'manage' ? 'active' : ''}"
117
63
  type="button"
118
64
  onclick={() => activeTab = 'manage'}
119
65
  >
120
- <i class="bi bi-trash me-2"></i>Manage Data
66
+ <i class="bi bi-database-gear me-2"></i>Manage Database
121
67
  </button>
122
68
  </div>
123
69
  </div>
@@ -132,118 +78,10 @@
132
78
  </div>
133
79
  {/if}
134
80
 
135
- <!-- Import JSON Tab -->
136
- {#if activeTab === 'import'}
137
- <div class="tab-pane fade show active">
138
- <div class="row">
139
- <div class="col-12">
140
- <h5 class="mb-3">Import Antenna Data from JSON</h5>
141
- <JsonImporter onDataRefresh={handleDataRefresh} />
142
- </div>
143
- </div>
144
- </div>
145
- {/if}
146
-
147
- <!-- Import MSI Tab -->
148
- {#if activeTab === 'msi'}
149
- <div class="tab-pane fade show active">
150
- <div class="row">
151
- <div class="col-12">
152
- <h5 class="mb-3">Import MSI Pattern Files</h5>
153
- <MSIConverter onDataRefresh={handleDataRefresh} {customBands} />
154
- </div>
155
- </div>
156
- </div>
157
- {/if}
158
-
159
- <!-- Export Data Tab -->
160
- {#if activeTab === 'export'}
161
- <div class="tab-pane fade show active">
162
- <div class="row">
163
- <div class="col-12">
164
- <h5 class="mb-3">Export Antenna Data</h5>
165
-
166
- <div class="card border-success">
167
- <div class="card-body text-center">
168
- <i class="bi bi-download text-success" style="font-size: 3rem;"></i>
169
- <h6 class="mt-3 mb-3">Download Data Backup</h6>
170
- <p class="text-muted mb-4">
171
- Export all antenna patterns and metadata as a JSON file.
172
- </p>
173
- <button
174
- type="button"
175
- class="btn btn-success btn-lg"
176
- onclick={handleExport}
177
- >
178
- <i class="bi bi-download me-2"></i>
179
- Export JSON File
180
- </button>
181
- </div>
182
- </div>
183
- </div>
184
- </div>
185
- </div>
186
- {/if}
187
-
188
- <!-- Manage Data Tab -->
81
+ <!-- Manage Database Tab -->
189
82
  {#if activeTab === 'manage'}
190
83
  <div class="tab-pane fade show active">
191
- <div class="row">
192
- <div class="col-12">
193
- <h5 class="mb-3">Manage Database</h5>
194
-
195
- <!-- Database Status -->
196
- <div class="card border-info mb-4">
197
- <div class="card-body">
198
- <h6 class="card-title">
199
- <i class="bi bi-info-circle me-2"></i>Database Status
200
- </h6>
201
- <DbNotification />
202
- </div>
203
- </div>
204
-
205
- <!-- Clear Data Section -->
206
- <div class="card border-danger">
207
- <div class="card-body">
208
- <p class="text-muted mb-3">
209
- Permanently delete all antenna data from the database.
210
- </p>
211
-
212
- {#if !showClearConfirm}
213
- <button
214
- type="button"
215
- class="btn btn-outline-danger"
216
- onclick={handleClearData}
217
- >
218
- <i class="bi bi-trash me-2"></i>
219
- Clear All Data
220
- </button>
221
- {:else}
222
- <div class="alert alert-danger" role="alert">
223
- <strong>Are you sure?</strong> This will permanently delete all antenna data.
224
- </div>
225
- <div class="d-flex gap-2">
226
- <button
227
- type="button"
228
- class="btn btn-danger"
229
- onclick={handleClearData}
230
- >
231
- <i class="bi bi-trash me-2"></i>
232
- Yes, Delete All Data
233
- </button>
234
- <button
235
- type="button"
236
- class="btn btn-secondary"
237
- onclick={cancelClear}
238
- >
239
- Cancel
240
- </button>
241
- </div>
242
- {/if}
243
- </div>
244
- </div>
245
- </div>
246
- </div>
84
+ <MSIConverter onDataRefresh={handleDataRefresh} {customBands} />
247
85
  </div>
248
86
  {/if}
249
87
 
@@ -296,12 +134,4 @@
296
134
  border-bottom-color: #0d6efd;
297
135
  background: none;
298
136
  }
299
-
300
- .card {
301
- transition: box-shadow 0.15s ease-in-out;
302
- }
303
-
304
- .card:hover {
305
- box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
306
- }
307
137
  </style>
@@ -134,8 +134,8 @@
134
134
  />
135
135
  </div>
136
136
  <div class="col-md-5">
137
- <label class="form-label">Filter by Band</label>
138
- <div class="btn-group w-100 flex-wrap" role="group">
137
+ <span class="form-label d-block">Filter by Band</span>
138
+ <div class="btn-group w-100 flex-wrap" role="group" aria-label="Filter by frequency band">
139
139
  <button
140
140
  type="button"
141
141
  class="btn btn-sm {selectedBand === null ? 'btn-primary' : 'btn-outline-primary'}"
@@ -1,9 +1,21 @@
1
1
  <svelte:options runes={true} />
2
2
 
3
3
  <script lang="ts">
4
+ import { untrack } from 'svelte';
5
+ import { SvelteSet } from 'svelte/reactivity';
4
6
  import { parseFolderWithErrors, type ParseProgress, type ParseError } from '../utils/msi-parser';
5
- import { saveAntennasWithPurge, type ImportResult } from '../utils/db-utils';
6
- import { STANDARD_BANDS } from '../band-config';
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 {
@@ -26,8 +38,25 @@
26
38
  let parseErrors = $state<ParseError[]>([]);
27
39
  let showErrorDetails = $state(false);
28
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);
45
+
29
46
  let recursiveScan = $state(true);
30
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
+
31
60
  async function selectFolder() {
32
61
  try {
33
62
  // Check if File System Access API is supported
@@ -96,14 +125,50 @@
96
125
  isLoading = true;
97
126
  message = 'Purging frequencies and saving to database...';
98
127
 
99
- // 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)
100
164
  importResult = await saveAntennasWithPurge(rawAntennas, customBands);
101
165
 
102
166
  if (importResult.success) {
103
167
  const stats = importResult.purgeStats;
104
- 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).`;
105
169
 
106
- // Notify parent of data refresh
170
+ // Refresh band counts and notify parent
171
+ await refreshBandCounts();
107
172
  onDataRefresh?.();
108
173
  } else {
109
174
  error = importResult.error || 'Failed to save antennas to database';
@@ -136,16 +201,125 @@
136
201
 
137
202
  message = 'JSON file downloaded (raw data, not purged).';
138
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));
139
306
  </script>
140
307
 
141
308
  <div class="card border-0 shadow-sm">
142
309
  <div class="card-body">
143
310
  <header class="d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3 mb-4">
144
311
  <div>
145
- <h1 class="h3 mb-2">Convert MSI / PNT to JSON</h1>
146
- <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>
147
322
  </div>
148
- <span class="badge text-bg-success">Local processing</span>
149
323
  </header>
150
324
 
151
325
  <!-- Bands Info -->
@@ -160,49 +334,134 @@
160
334
  </div>
161
335
 
162
336
  <div class="d-flex flex-column gap-4">
163
- <div class="text-center">
164
- <button class="btn btn-primary btn-lg px-4" onclick={selectFolder} disabled={isLoading}>
165
- {#if isLoading}
166
- <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
167
- Processing…
168
- {:else}
169
- <i class="bi bi-folder-plus me-2"></i>
170
- Choose folder with MSI / PNT files
171
- {/if}
172
- </button>
173
- </div>
174
-
175
- <div class="d-flex justify-content-center">
176
- <div class="form-check form-switch">
177
- <input class="form-check-input" type="checkbox" bind:checked={recursiveScan} id="recursiveScan" />
178
- <label class="form-check-label" for="recursiveScan">
179
- <i class="bi bi-arrow-down-circle me-1"></i>
180
- Include sub-folders during scan
181
- </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
182
341
  </div>
183
- </div>
184
-
185
- {#if rawAntennas.length > 0}
186
- <div class="row g-3">
187
- <div class="col-md-6">
188
- <button class="btn btn-success w-100" onclick={saveToDatabase} disabled={isLoading}>
189
- {#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}
190
346
  <span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
191
- Saving
347
+ Processing
192
348
  {:else}
193
- <i class="bi bi-database-add me-2"></i>
194
- Import to database (with purge)
349
+ <i class="bi bi-folder-plus me-2"></i>
350
+ Choose folder with MSI / PNT files
195
351
  {/if}
196
352
  </button>
197
353
  </div>
198
- <div class="col-md-6">
199
- <button class="btn btn-outline-secondary w-100" onclick={downloadJson}>
200
- <i class="bi bi-download me-2"></i>
201
- Download raw JSON
202
- </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>
203
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}
204
391
  </div>
205
- {/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>
206
465
 
207
466
  <!-- Progress indicator during parsing -->
208
467
  {#if isLoading && parseProgress}
@@ -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
  */