@smartnet360/svelte-components 0.0.124 → 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.
@@ -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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,397 @@
1
+ <script lang="ts">
2
+ import type { AuthState } from './auth.svelte.js';
3
+
4
+ interface Props {
5
+ /** Auth state instance */
6
+ auth: AuthState;
7
+ /** Title for login form */
8
+ title?: string;
9
+ /** Subtitle/description */
10
+ subtitle?: string;
11
+ /** Logo URL (optional) */
12
+ logo?: string;
13
+ /** Callback on successful login */
14
+ onLogin?: () => void;
15
+ }
16
+
17
+ let {
18
+ auth,
19
+ title = 'Welcome',
20
+ subtitle = 'Sign in to access your tools',
21
+ logo,
22
+ onLogin
23
+ }: Props = $props();
24
+
25
+ // Form state
26
+ let username = $state('');
27
+ let password = $state('');
28
+ let agreedToMonitoring = $state(false);
29
+ let showPassword = $state(false);
30
+
31
+ // Form validation
32
+ const isFormValid = $derived(
33
+ username.trim().length > 0 &&
34
+ password.length > 0 &&
35
+ agreedToMonitoring
36
+ );
37
+
38
+ async function handleSubmit(e: SubmitEvent) {
39
+ e.preventDefault();
40
+
41
+ if (!isFormValid) return;
42
+
43
+ const success = await auth.login(username.trim(), password, agreedToMonitoring);
44
+
45
+ if (success) {
46
+ username = '';
47
+ password = '';
48
+ agreedToMonitoring = false;
49
+ onLogin?.();
50
+ }
51
+ }
52
+
53
+ function togglePassword() {
54
+ showPassword = !showPassword;
55
+ }
56
+ </script>
57
+
58
+ <div class="login-page">
59
+ <div class="login-card">
60
+ <!-- Header -->
61
+ <div class="login-header">
62
+ {#if logo}
63
+ <img src={logo} alt="Logo" class="login-logo" />
64
+ {:else}
65
+ <div class="login-icon">
66
+ <i class="bi bi-broadcast-pin"></i>
67
+ </div>
68
+ {/if}
69
+ <h1>{title}</h1>
70
+ <p>{subtitle}</p>
71
+ </div>
72
+
73
+ <!-- Error -->
74
+ {#if auth.error}
75
+ <div class="login-error">
76
+ <i class="bi bi-exclamation-circle"></i>
77
+ <span>{auth.error}</span>
78
+ <button type="button" onclick={() => auth.clearError()} aria-label="Dismiss error">
79
+ <i class="bi bi-x"></i>
80
+ </button>
81
+ </div>
82
+ {/if}
83
+
84
+ <!-- Form -->
85
+ <form onsubmit={handleSubmit} class="login-form">
86
+ <div class="form-field">
87
+ <label for="username">Username</label>
88
+ <div class="input-wrapper">
89
+ <i class="bi bi-person"></i>
90
+ <input
91
+ type="text"
92
+ id="username"
93
+ bind:value={username}
94
+ placeholder="Enter username"
95
+ autocomplete="username"
96
+ disabled={auth.isLoading}
97
+ />
98
+ </div>
99
+ </div>
100
+
101
+ <div class="form-field">
102
+ <label for="password">Password</label>
103
+ <div class="input-wrapper">
104
+ <i class="bi bi-lock"></i>
105
+ <input
106
+ type={showPassword ? 'text' : 'password'}
107
+ id="password"
108
+ bind:value={password}
109
+ placeholder="Enter password"
110
+ autocomplete="current-password"
111
+ disabled={auth.isLoading}
112
+ />
113
+ <button
114
+ type="button"
115
+ class="toggle-password"
116
+ onclick={togglePassword}
117
+ tabindex={-1}
118
+ aria-label={showPassword ? 'Hide password' : 'Show password'}
119
+ >
120
+ <i class="bi {showPassword ? 'bi-eye-slash' : 'bi-eye'}"></i>
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ <label class="checkbox-field">
126
+ <input
127
+ type="checkbox"
128
+ bind:checked={agreedToMonitoring}
129
+ disabled={auth.isLoading}
130
+ />
131
+ <span class="checkmark"></span>
132
+ <span class="checkbox-text">
133
+ I agree to activity monitoring for security purposes
134
+ </span>
135
+ </label>
136
+
137
+ <button
138
+ type="submit"
139
+ class="login-button"
140
+ disabled={!isFormValid || auth.isLoading}
141
+ >
142
+ {#if auth.isLoading}
143
+ <span class="spinner"></span>
144
+ Signing in...
145
+ {:else}
146
+ Sign In
147
+ <i class="bi bi-arrow-right"></i>
148
+ {/if}
149
+ </button>
150
+ </form>
151
+
152
+ <div class="login-footer">
153
+ <i class="bi bi-shield-check"></i>
154
+ LDAP Authentication
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <style>
160
+ .login-page {
161
+ min-height: 100vh;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ padding: 1.5rem;
166
+ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
167
+ }
168
+
169
+ .login-card {
170
+ width: 100%;
171
+ max-width: 380px;
172
+ background: #fff;
173
+ border-radius: 16px;
174
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
175
+ overflow: hidden;
176
+ }
177
+
178
+ .login-header {
179
+ text-align: center;
180
+ padding: 2.5rem 2rem 1.5rem;
181
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
182
+ color: #fff;
183
+ }
184
+
185
+ .login-logo {
186
+ height: 48px;
187
+ margin-bottom: 1rem;
188
+ }
189
+
190
+ .login-icon {
191
+ font-size: 2.5rem;
192
+ margin-bottom: 0.75rem;
193
+ }
194
+
195
+ .login-header h1 {
196
+ font-size: 1.5rem;
197
+ font-weight: 600;
198
+ margin: 0 0 0.25rem;
199
+ }
200
+
201
+ .login-header p {
202
+ font-size: 0.875rem;
203
+ opacity: 0.85;
204
+ margin: 0;
205
+ }
206
+
207
+ .login-error {
208
+ display: flex;
209
+ align-items: center;
210
+ gap: 0.5rem;
211
+ margin: 1rem 1.5rem 0;
212
+ padding: 0.75rem 1rem;
213
+ background: #fef2f2;
214
+ border: 1px solid #fecaca;
215
+ border-radius: 8px;
216
+ color: #dc2626;
217
+ font-size: 0.875rem;
218
+ }
219
+
220
+ .login-error button {
221
+ margin-left: auto;
222
+ background: none;
223
+ border: none;
224
+ color: inherit;
225
+ cursor: pointer;
226
+ padding: 0;
227
+ font-size: 1.25rem;
228
+ line-height: 1;
229
+ }
230
+
231
+ .login-form {
232
+ padding: 1.5rem 2rem 2rem;
233
+ }
234
+
235
+ .form-field {
236
+ margin-bottom: 1.25rem;
237
+ }
238
+
239
+ .form-field label {
240
+ display: block;
241
+ font-size: 0.8125rem;
242
+ font-weight: 500;
243
+ color: #374151;
244
+ margin-bottom: 0.5rem;
245
+ }
246
+
247
+ .input-wrapper {
248
+ position: relative;
249
+ display: flex;
250
+ align-items: center;
251
+ }
252
+
253
+ .input-wrapper > i:first-child {
254
+ position: absolute;
255
+ left: 0.875rem;
256
+ color: #9ca3af;
257
+ font-size: 1rem;
258
+ }
259
+
260
+ .input-wrapper input {
261
+ width: 100%;
262
+ padding: 0.75rem 0.875rem 0.75rem 2.5rem;
263
+ border: 1px solid #e5e7eb;
264
+ border-radius: 8px;
265
+ font-size: 0.9375rem;
266
+ transition: border-color 0.15s, box-shadow 0.15s;
267
+ }
268
+
269
+ .input-wrapper input:focus {
270
+ outline: none;
271
+ border-color: #3b82f6;
272
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
273
+ }
274
+
275
+ .input-wrapper input:disabled {
276
+ background: #f9fafb;
277
+ cursor: not-allowed;
278
+ }
279
+
280
+ .toggle-password {
281
+ position: absolute;
282
+ right: 0.5rem;
283
+ background: none;
284
+ border: none;
285
+ color: #9ca3af;
286
+ cursor: pointer;
287
+ padding: 0.5rem;
288
+ font-size: 1rem;
289
+ }
290
+
291
+ .toggle-password:hover {
292
+ color: #6b7280;
293
+ }
294
+
295
+ .checkbox-field {
296
+ display: flex;
297
+ align-items: flex-start;
298
+ gap: 0.625rem;
299
+ cursor: pointer;
300
+ margin-bottom: 1.5rem;
301
+ }
302
+
303
+ .checkbox-field input {
304
+ position: absolute;
305
+ opacity: 0;
306
+ pointer-events: none;
307
+ }
308
+
309
+ .checkmark {
310
+ flex-shrink: 0;
311
+ width: 18px;
312
+ height: 18px;
313
+ border: 2px solid #d1d5db;
314
+ border-radius: 4px;
315
+ transition: all 0.15s;
316
+ display: flex;
317
+ align-items: center;
318
+ justify-content: center;
319
+ }
320
+
321
+ .checkbox-field input:checked + .checkmark {
322
+ background: #3b82f6;
323
+ border-color: #3b82f6;
324
+ }
325
+
326
+ .checkbox-field input:checked + .checkmark::after {
327
+ content: '';
328
+ width: 5px;
329
+ height: 9px;
330
+ border: solid #fff;
331
+ border-width: 0 2px 2px 0;
332
+ transform: rotate(45deg);
333
+ margin-bottom: 2px;
334
+ }
335
+
336
+ .checkbox-text {
337
+ font-size: 0.8125rem;
338
+ color: #4b5563;
339
+ line-height: 1.4;
340
+ }
341
+
342
+ .login-button {
343
+ width: 100%;
344
+ padding: 0.875rem 1.5rem;
345
+ background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
346
+ color: #fff;
347
+ border: none;
348
+ border-radius: 8px;
349
+ font-size: 0.9375rem;
350
+ font-weight: 500;
351
+ cursor: pointer;
352
+ display: flex;
353
+ align-items: center;
354
+ justify-content: center;
355
+ gap: 0.5rem;
356
+ transition: opacity 0.15s, transform 0.15s;
357
+ }
358
+
359
+ .login-button:hover:not(:disabled) {
360
+ opacity: 0.9;
361
+ }
362
+
363
+ .login-button:active:not(:disabled) {
364
+ transform: scale(0.98);
365
+ }
366
+
367
+ .login-button:disabled {
368
+ opacity: 0.5;
369
+ cursor: not-allowed;
370
+ }
371
+
372
+ .spinner {
373
+ width: 16px;
374
+ height: 16px;
375
+ border: 2px solid rgba(255, 255, 255, 0.3);
376
+ border-top-color: #fff;
377
+ border-radius: 50%;
378
+ animation: spin 0.8s linear infinite;
379
+ }
380
+
381
+ @keyframes spin {
382
+ to { transform: rotate(360deg); }
383
+ }
384
+
385
+ .login-footer {
386
+ text-align: center;
387
+ padding: 1rem;
388
+ background: #f8fafc;
389
+ border-top: 1px solid #e5e7eb;
390
+ font-size: 0.75rem;
391
+ color: #64748b;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ gap: 0.375rem;
396
+ }
397
+ </style>
@@ -0,0 +1,16 @@
1
+ import type { AuthState } from './auth.svelte.js';
2
+ interface Props {
3
+ /** Auth state instance */
4
+ auth: AuthState;
5
+ /** Title for login form */
6
+ title?: string;
7
+ /** Subtitle/description */
8
+ subtitle?: string;
9
+ /** Logo URL (optional) */
10
+ logo?: string;
11
+ /** Callback on successful login */
12
+ onLogin?: () => void;
13
+ }
14
+ declare const LoginForm: import("svelte").Component<Props, {}, "">;
15
+ type LoginForm = ReturnType<typeof LoginForm>;
16
+ export default LoginForm;
@@ -0,0 +1,22 @@
1
+ import type { UserSession, User, FeatureAccess, AuthConfig } from './types.js';
2
+ /**
3
+ * Create auth state store with Svelte 5 runes
4
+ * This must be called within a component context or .svelte.ts file
5
+ */
6
+ export declare function createAuthState(config?: Partial<AuthConfig>): {
7
+ readonly session: UserSession | null;
8
+ readonly isLoading: boolean;
9
+ readonly error: string | null;
10
+ readonly initialized: boolean;
11
+ readonly isAuthenticated: boolean;
12
+ readonly user: User | null;
13
+ readonly permissions: FeatureAccess[];
14
+ readonly token: string | null;
15
+ login: (username: string, password: string, agreedToMonitoring: boolean) => Promise<boolean>;
16
+ loginDev: (devUser: User, devPermissions: FeatureAccess[]) => void;
17
+ logout: () => Promise<void>;
18
+ hasPermission: (featureId: string, level?: "view" | "edit" | "admin") => boolean;
19
+ canAccessRoute: (route: string, requiredPermission?: string) => boolean;
20
+ clearError: () => void;
21
+ };
22
+ export type AuthState = ReturnType<typeof createAuthState>;