winebox 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl

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.
winebox/static/js/app.js CHANGED
@@ -9,6 +9,7 @@ const API_BASE = '/api';
9
9
  let currentPage = 'dashboard';
10
10
  let authToken = localStorage.getItem('winebox_token');
11
11
  let currentUser = null;
12
+ let lastScanResult = null; // Store last scan result to avoid rescanning on checkin
12
13
 
13
14
  // Initialize app
14
15
  document.addEventListener('DOMContentLoaded', () => {
@@ -17,8 +18,23 @@ document.addEventListener('DOMContentLoaded', () => {
17
18
  initModals();
18
19
  initAuth();
19
20
  checkAuth();
21
+ loadAppInfo();
20
22
  });
21
23
 
24
+ // Load app info for footer
25
+ async function loadAppInfo() {
26
+ try {
27
+ const response = await fetch('/health');
28
+ const data = await response.json();
29
+ const appInfo = document.getElementById('app-info');
30
+ if (appInfo && data.app_name && data.version) {
31
+ appInfo.innerHTML = `${data.app_name} <span class="version">v${data.version}</span>`;
32
+ }
33
+ } catch (error) {
34
+ console.log('Could not load app info');
35
+ }
36
+ }
37
+
22
38
  // Authentication
23
39
  function initAuth() {
24
40
  // Login form
@@ -26,6 +42,38 @@ function initAuth() {
26
42
 
27
43
  // Logout button
28
44
  document.getElementById('logout-btn').addEventListener('click', handleLogout);
45
+
46
+ // Username link to settings
47
+ document.getElementById('username-display').addEventListener('click', (e) => {
48
+ e.preventDefault();
49
+ navigateTo('settings');
50
+ });
51
+
52
+ // Password toggle for all password fields
53
+ initPasswordToggles();
54
+ }
55
+
56
+ function initPasswordToggles() {
57
+ document.querySelectorAll('.password-toggle').forEach(toggle => {
58
+ toggle.addEventListener('click', function() {
59
+ const wrapper = this.closest('.password-input-wrapper');
60
+ const passwordInput = wrapper.querySelector('input[type="password"], input[type="text"]');
61
+ const eyeIcon = this.querySelector('.eye-icon');
62
+ const eyeOffIcon = this.querySelector('.eye-off-icon');
63
+
64
+ if (passwordInput.type === 'password') {
65
+ passwordInput.type = 'text';
66
+ eyeIcon.style.display = 'none';
67
+ eyeOffIcon.style.display = 'block';
68
+ this.setAttribute('aria-label', 'Hide password');
69
+ } else {
70
+ passwordInput.type = 'password';
71
+ eyeIcon.style.display = 'block';
72
+ eyeOffIcon.style.display = 'none';
73
+ this.setAttribute('aria-label', 'Show password');
74
+ }
75
+ });
76
+ });
29
77
  }
30
78
 
31
79
  async function checkAuth() {
@@ -58,7 +106,9 @@ function showMainApp() {
58
106
  document.body.classList.remove('logged-out');
59
107
  document.getElementById('page-login').classList.remove('active');
60
108
  document.getElementById('user-info').style.display = 'flex';
61
- document.getElementById('username-display').textContent = currentUser.username;
109
+ // Display full name if available, otherwise username
110
+ const displayName = currentUser.full_name || currentUser.username;
111
+ document.getElementById('username-display').textContent = displayName;
62
112
  loadDashboard();
63
113
  }
64
114
 
@@ -166,6 +216,9 @@ function navigateTo(page) {
166
216
  case 'search':
167
217
  // Search results loaded on form submit
168
218
  break;
219
+ case 'settings':
220
+ loadSettings();
221
+ break;
169
222
  }
170
223
  }
171
224
 
@@ -177,6 +230,8 @@ function initForms() {
177
230
  checkinForm.addEventListener('reset', () => {
178
231
  document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
179
232
  document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
233
+ clearRawLabelText();
234
+ lastScanResult = null; // Clear stored scan result
180
235
  });
181
236
 
182
237
  // Image previews - make clickable to trigger file input
@@ -187,9 +242,11 @@ function initForms() {
187
242
 
188
243
  frontLabel.addEventListener('change', (e) => {
189
244
  previewImage(e.target, 'front-preview');
245
+ scanLabels();
190
246
  });
191
247
  backLabel.addEventListener('change', (e) => {
192
248
  previewImage(e.target, 'back-preview');
249
+ scanLabels();
193
250
  });
194
251
 
195
252
  // Click on preview to trigger file input
@@ -200,6 +257,25 @@ function initForms() {
200
257
  backLabel.click();
201
258
  });
202
259
 
260
+ // Label text collapsible toggle
261
+ const labelTextToggle = document.getElementById('label-text-toggle');
262
+ if (labelTextToggle) {
263
+ labelTextToggle.addEventListener('click', () => {
264
+ const section = document.getElementById('label-text-section');
265
+ const content = document.getElementById('label-text-content');
266
+ const icon = section.querySelector('.collapse-icon');
267
+
268
+ section.classList.toggle('open');
269
+ if (section.classList.contains('open')) {
270
+ content.style.display = 'block';
271
+ icon.textContent = '-';
272
+ } else {
273
+ content.style.display = 'none';
274
+ icon.textContent = '+';
275
+ }
276
+ });
277
+ }
278
+
203
279
  // Search form
204
280
  document.getElementById('search-form').addEventListener('submit', handleSearch);
205
281
 
@@ -212,6 +288,12 @@ function initForms() {
212
288
 
213
289
  // History filter
214
290
  document.getElementById('history-filter').addEventListener('change', loadHistory);
291
+
292
+ // Settings forms
293
+ document.getElementById('profile-form').addEventListener('submit', handleProfileUpdate);
294
+ document.getElementById('password-form').addEventListener('submit', handlePasswordChange);
295
+ document.getElementById('api-key-form').addEventListener('submit', handleApiKeyUpdate);
296
+ document.getElementById('delete-api-key-btn').addEventListener('click', handleApiKeyDelete);
215
297
  }
216
298
 
217
299
  function previewImage(input, previewId) {
@@ -227,10 +309,279 @@ function previewImage(input, previewId) {
227
309
  }
228
310
  }
229
311
 
230
- async function handleCheckin(e) {
312
+ async function scanLabels() {
313
+ const frontLabel = document.getElementById('front-label');
314
+
315
+ // Only scan if front label is present
316
+ if (!frontLabel.files || !frontLabel.files[0]) {
317
+ return;
318
+ }
319
+
320
+ const backLabel = document.getElementById('back-label');
321
+ const formData = new FormData();
322
+ formData.append('front_label', frontLabel.files[0]);
323
+
324
+ if (backLabel.files && backLabel.files[0]) {
325
+ formData.append('back_label', backLabel.files[0]);
326
+ }
327
+
328
+ // Show scanning indicator
329
+ showScanningIndicator(true);
330
+
331
+ try {
332
+ const response = await fetchWithAuth(`${API_BASE}/wines/scan`, {
333
+ method: 'POST',
334
+ body: formData
335
+ });
336
+
337
+ if (!response.ok) {
338
+ const error = await response.json();
339
+ throw new Error(error.detail || 'Scan failed');
340
+ }
341
+
342
+ const result = await response.json();
343
+ lastScanResult = result; // Store for checkin
344
+ populateFormFromScan(result);
345
+ const methodName = result.method === 'claude_vision' ? 'Claude Vision' : 'Tesseract OCR';
346
+ showToast(`Label scanned with ${methodName}`, 'success');
347
+ } catch (error) {
348
+ showToast(`Scan failed: ${error.message}`, 'error');
349
+ } finally {
350
+ showScanningIndicator(false);
351
+ }
352
+ }
353
+
354
+ function populateFormFromScan(result) {
355
+ const parsed = result.parsed;
356
+
357
+ // Update fields with scanned values (overwrites previous scan results)
358
+ const fields = {
359
+ 'wine-name': parsed.name,
360
+ 'winery': parsed.winery,
361
+ 'vintage': parsed.vintage,
362
+ 'grape-variety': parsed.grape_variety,
363
+ 'region': parsed.region,
364
+ 'country': parsed.country,
365
+ 'alcohol': parsed.alcohol_percentage
366
+ };
367
+
368
+ for (const [fieldId, value] of Object.entries(fields)) {
369
+ const input = document.getElementById(fieldId);
370
+ if (input && value !== null && value !== undefined) {
371
+ input.value = value;
372
+ // Add visual indicator that field was auto-filled
373
+ input.classList.add('auto-filled');
374
+ setTimeout(() => input.classList.remove('auto-filled'), 2000);
375
+ }
376
+ }
377
+
378
+ // Populate raw label text section
379
+ populateRawLabelText(result.ocr, result.method);
380
+ }
381
+
382
+ function populateRawLabelText(ocr, method) {
383
+ const section = document.getElementById('label-text-section');
384
+ const frontText = document.getElementById('raw-front-label-text');
385
+ const backSection = document.getElementById('raw-back-label-section');
386
+ const backText = document.getElementById('raw-back-label-text');
387
+ const header = section.querySelector('h3');
388
+
389
+ // Update header to show scan method
390
+ const methodName = method === 'claude_vision' ? 'Claude Vision' : 'Tesseract OCR';
391
+ header.innerHTML = `Raw Label Text <span class="scan-method-badge">${methodName}</span>`;
392
+
393
+ if (ocr.front_label_text) {
394
+ frontText.textContent = ocr.front_label_text;
395
+ section.style.display = 'block';
396
+ }
397
+
398
+ if (ocr.back_label_text) {
399
+ backText.textContent = ocr.back_label_text;
400
+ backSection.style.display = 'block';
401
+ } else {
402
+ backSection.style.display = 'none';
403
+ }
404
+ }
405
+
406
+ function clearRawLabelText() {
407
+ const section = document.getElementById('label-text-section');
408
+ const frontText = document.getElementById('raw-front-label-text');
409
+ const backSection = document.getElementById('raw-back-label-section');
410
+ const backText = document.getElementById('raw-back-label-text');
411
+
412
+ section.style.display = 'none';
413
+ section.classList.remove('open');
414
+ document.getElementById('label-text-content').style.display = 'none';
415
+ document.querySelector('#label-text-section .collapse-icon').textContent = '+';
416
+ frontText.textContent = '';
417
+ backText.textContent = '';
418
+ backSection.style.display = 'none';
419
+ }
420
+
421
+ function showScanningIndicator(show) {
422
+ const submitBtn = document.querySelector('#checkin-form button[type="submit"]');
423
+ const formNote = document.querySelector('#checkin-form .form-note');
424
+
425
+ if (show) {
426
+ if (submitBtn) {
427
+ submitBtn.disabled = true;
428
+ submitBtn.dataset.originalText = submitBtn.textContent;
429
+ submitBtn.textContent = 'Scanning...';
430
+ }
431
+ if (formNote) {
432
+ formNote.dataset.originalText = formNote.textContent;
433
+ formNote.textContent = 'Analyzing label with Claude Vision...';
434
+ formNote.classList.add('scanning');
435
+ }
436
+ } else {
437
+ if (submitBtn) {
438
+ submitBtn.disabled = false;
439
+ submitBtn.textContent = submitBtn.dataset.originalText || 'Check In Wine';
440
+ }
441
+ if (formNote) {
442
+ formNote.textContent = formNote.dataset.originalText || 'Leave fields blank to use OCR-detected values';
443
+ formNote.classList.remove('scanning');
444
+ }
445
+ }
446
+ }
447
+
448
+ // Store pending checkin data for confirmation
449
+ let pendingCheckinData = null;
450
+
451
+ function handleCheckin(e) {
231
452
  e.preventDefault();
232
- const form = e.target;
233
- const formData = new FormData(form);
453
+
454
+ const frontLabel = document.getElementById('front-label');
455
+ if (!frontLabel.files || !frontLabel.files[0]) {
456
+ showToast('Please select a front label image', 'error');
457
+ return;
458
+ }
459
+
460
+ // Store the form data for later submission
461
+ pendingCheckinData = {
462
+ frontLabel: frontLabel.files[0],
463
+ backLabel: document.getElementById('back-label').files?.[0] || null,
464
+ name: document.getElementById('wine-name').value,
465
+ winery: document.getElementById('winery').value,
466
+ vintage: document.getElementById('vintage').value,
467
+ grapeVariety: document.getElementById('grape-variety').value,
468
+ region: document.getElementById('region').value,
469
+ country: document.getElementById('country').value,
470
+ alcohol: document.getElementById('alcohol').value,
471
+ quantity: document.getElementById('quantity').value || '1',
472
+ notes: document.getElementById('notes').value,
473
+ frontLabelText: lastScanResult?.ocr?.front_label_text || '',
474
+ backLabelText: lastScanResult?.ocr?.back_label_text || ''
475
+ };
476
+
477
+ // Show the confirmation modal with editable fields
478
+ showCheckinConfirmation();
479
+ }
480
+
481
+ function showCheckinConfirmation() {
482
+ const modal = document.getElementById('checkin-confirm-modal');
483
+ const data = pendingCheckinData;
484
+
485
+ // Set image preview
486
+ const imageContainer = document.getElementById('checkin-confirm-image');
487
+ if (data.frontLabel) {
488
+ const reader = new FileReader();
489
+ reader.onload = (e) => {
490
+ imageContainer.innerHTML = `<img src="${e.target.result}" alt="Wine label">`;
491
+ };
492
+ reader.readAsDataURL(data.frontLabel);
493
+ } else {
494
+ imageContainer.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);">No image</div>';
495
+ }
496
+
497
+ // Populate editable fields
498
+ document.getElementById('confirm-wine-name').value = data.name || '';
499
+ document.getElementById('confirm-winery').value = data.winery || '';
500
+ document.getElementById('confirm-vintage').value = data.vintage || '';
501
+ document.getElementById('confirm-grape-variety').value = data.grapeVariety || '';
502
+ document.getElementById('confirm-region').value = data.region || '';
503
+ document.getElementById('confirm-country').value = data.country || '';
504
+ document.getElementById('confirm-alcohol').value = data.alcohol || '';
505
+ document.getElementById('confirm-quantity').value = data.quantity || '1';
506
+ document.getElementById('confirm-notes').value = data.notes || '';
507
+
508
+ // Set OCR text (hidden by default)
509
+ const ocrSection = document.getElementById('confirm-ocr-section');
510
+ const ocrContent = document.getElementById('confirm-ocr-content');
511
+ const ocrToggle = document.getElementById('confirm-ocr-toggle');
512
+
513
+ if (data.frontLabelText) {
514
+ document.getElementById('checkin-confirm-front-ocr').textContent = data.frontLabelText;
515
+ ocrSection.style.display = 'block';
516
+ ocrContent.style.display = 'none'; // Hidden by default
517
+ ocrSection.classList.remove('open');
518
+ ocrToggle.querySelector('.collapse-icon').textContent = '+';
519
+ ocrToggle.querySelector('.label').textContent = 'Show Raw Label Text';
520
+ } else {
521
+ ocrSection.style.display = 'none';
522
+ }
523
+
524
+ const backOcrSection = document.getElementById('checkin-confirm-back-ocr-section');
525
+ if (data.backLabelText) {
526
+ backOcrSection.style.display = 'block';
527
+ document.getElementById('checkin-confirm-back-ocr').textContent = data.backLabelText;
528
+ } else {
529
+ backOcrSection.style.display = 'none';
530
+ }
531
+
532
+ // Show modal
533
+ modal.classList.add('active');
534
+
535
+ // Set up OCR toggle
536
+ ocrToggle.onclick = () => {
537
+ ocrSection.classList.toggle('open');
538
+ if (ocrSection.classList.contains('open')) {
539
+ ocrContent.style.display = 'block';
540
+ ocrToggle.querySelector('.collapse-icon').textContent = '-';
541
+ ocrToggle.querySelector('.label').textContent = 'Hide Raw Label Text';
542
+ } else {
543
+ ocrContent.style.display = 'none';
544
+ ocrToggle.querySelector('.collapse-icon').textContent = '+';
545
+ ocrToggle.querySelector('.label').textContent = 'Show Raw Label Text';
546
+ }
547
+ };
548
+
549
+ // Set up button handlers
550
+ document.getElementById('checkin-confirm-btn').onclick = submitCheckin;
551
+ document.getElementById('checkin-cancel-btn').onclick = cancelCheckin;
552
+ }
553
+
554
+ async function submitCheckin() {
555
+ const modal = document.getElementById('checkin-confirm-modal');
556
+ const data = pendingCheckinData;
557
+
558
+ // Build form data from confirmation modal fields
559
+ const formData = new FormData();
560
+ formData.append('front_label', data.frontLabel);
561
+ if (data.backLabel) {
562
+ formData.append('back_label', data.backLabel);
563
+ }
564
+
565
+ // Get values from confirmation modal (may have been edited)
566
+ formData.append('name', document.getElementById('confirm-wine-name').value);
567
+ formData.append('winery', document.getElementById('confirm-winery').value);
568
+ const vintage = document.getElementById('confirm-vintage').value;
569
+ if (vintage) formData.append('vintage', vintage);
570
+ formData.append('grape_variety', document.getElementById('confirm-grape-variety').value);
571
+ formData.append('region', document.getElementById('confirm-region').value);
572
+ formData.append('country', document.getElementById('confirm-country').value);
573
+ const alcohol = document.getElementById('confirm-alcohol').value;
574
+ if (alcohol) formData.append('alcohol_percentage', alcohol);
575
+ formData.append('quantity', document.getElementById('confirm-quantity').value || '1');
576
+ formData.append('notes', document.getElementById('confirm-notes').value);
577
+
578
+ // Include pre-scanned OCR text to avoid rescanning (saves API costs)
579
+ if (data.frontLabelText) {
580
+ formData.append('front_label_text', data.frontLabelText);
581
+ }
582
+ if (data.backLabelText) {
583
+ formData.append('back_label_text', data.backLabelText);
584
+ }
234
585
 
235
586
  try {
236
587
  const response = await fetchWithAuth(`${API_BASE}/wines/checkin`, {
@@ -245,13 +596,29 @@ async function handleCheckin(e) {
245
596
 
246
597
  const wine = await response.json();
247
598
  showToast(`Successfully checked in: ${wine.name}`, 'success');
248
- form.reset();
599
+
600
+ // Close modal and reset form
601
+ modal.classList.remove('active');
602
+ document.getElementById('checkin-form').reset();
603
+ document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
604
+ document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
605
+ clearRawLabelText();
606
+ lastScanResult = null;
607
+ pendingCheckinData = null;
608
+
609
+ // Navigate to cellar
249
610
  navigateTo('cellar');
250
611
  } catch (error) {
251
612
  showToast(error.message, 'error');
252
613
  }
253
614
  }
254
615
 
616
+ function cancelCheckin() {
617
+ const modal = document.getElementById('checkin-confirm-modal');
618
+ modal.classList.remove('active');
619
+ // Keep the form data so user can make changes and try again
620
+ }
621
+
255
622
  async function handleSearch(e) {
256
623
  e.preventDefault();
257
624
  const form = e.target;
@@ -558,9 +925,25 @@ async function showWineDetail(wineId) {
558
925
  ` : ''}
559
926
 
560
927
  ${wine.front_label_text ? `
561
- <div class="wine-detail-field">
562
- <div class="label">OCR Text (Front Label)</div>
563
- <div class="wine-detail-ocr">${wine.front_label_text}</div>
928
+ <div class="wine-detail-label-text collapsible">
929
+ <div class="collapsible-header" onclick="toggleWineDetailLabelText(this)">
930
+ <span class="label">Show Raw Label Text</span>
931
+ <span class="collapse-icon">+</span>
932
+ </div>
933
+ <div class="collapsible-content" style="display: none;">
934
+ <div class="ocr-raw-text">
935
+ <div class="ocr-raw-section">
936
+ <label>Front Label:</label>
937
+ <pre>${escapeHtml(wine.front_label_text)}</pre>
938
+ </div>
939
+ ${wine.back_label_text ? `
940
+ <div class="ocr-raw-section">
941
+ <label>Back Label:</label>
942
+ <pre>${escapeHtml(wine.back_label_text)}</pre>
943
+ </div>
944
+ ` : ''}
945
+ </div>
946
+ </div>
564
947
  </div>
565
948
  ` : ''}
566
949
 
@@ -678,6 +1061,30 @@ function formatDate(dateString) {
678
1061
  });
679
1062
  }
680
1063
 
1064
+ function escapeHtml(text) {
1065
+ const div = document.createElement('div');
1066
+ div.textContent = text;
1067
+ return div.innerHTML;
1068
+ }
1069
+
1070
+ function toggleWineDetailLabelText(header) {
1071
+ const section = header.parentElement;
1072
+ const content = section.querySelector('.collapsible-content');
1073
+ const icon = header.querySelector('.collapse-icon');
1074
+ const label = header.querySelector('.label');
1075
+
1076
+ section.classList.toggle('open');
1077
+ if (section.classList.contains('open')) {
1078
+ content.style.display = 'block';
1079
+ icon.textContent = '-';
1080
+ label.textContent = 'Hide Raw Label Text';
1081
+ } else {
1082
+ content.style.display = 'none';
1083
+ icon.textContent = '+';
1084
+ label.textContent = 'Show Raw Label Text';
1085
+ }
1086
+ }
1087
+
681
1088
  function showToast(message, type = 'info') {
682
1089
  const container = document.getElementById('toast-container');
683
1090
  const toast = document.createElement('div');
@@ -701,3 +1108,171 @@ function debounce(func, wait) {
701
1108
  timeout = setTimeout(later, wait);
702
1109
  };
703
1110
  }
1111
+
1112
+ // Settings
1113
+ function loadSettings() {
1114
+ // Populate profile form with current user data
1115
+ document.getElementById('settings-username').value = currentUser.username;
1116
+ document.getElementById('settings-fullname').value = currentUser.full_name || '';
1117
+
1118
+ // Update API key status
1119
+ updateApiKeyStatus(currentUser.has_api_key);
1120
+
1121
+ // Clear password form
1122
+ document.getElementById('password-form').reset();
1123
+
1124
+ // Clear API key form
1125
+ document.getElementById('api-key').value = '';
1126
+ }
1127
+
1128
+ function updateApiKeyStatus(hasApiKey) {
1129
+ const statusDiv = document.getElementById('api-key-status');
1130
+ const statusText = statusDiv.querySelector('.status-text');
1131
+ const deleteBtn = document.getElementById('delete-api-key-btn');
1132
+
1133
+ statusDiv.classList.remove('configured', 'not-configured');
1134
+
1135
+ if (hasApiKey) {
1136
+ statusDiv.classList.add('configured');
1137
+ statusText.textContent = 'API key is configured';
1138
+ deleteBtn.style.display = 'inline-block';
1139
+ } else {
1140
+ statusDiv.classList.add('not-configured');
1141
+ statusText.textContent = 'No API key configured';
1142
+ deleteBtn.style.display = 'none';
1143
+ }
1144
+ }
1145
+
1146
+ async function handleProfileUpdate(e) {
1147
+ e.preventDefault();
1148
+
1149
+ const fullName = document.getElementById('settings-fullname').value.trim();
1150
+
1151
+ try {
1152
+ const response = await fetchWithAuth(`${API_BASE}/auth/profile`, {
1153
+ method: 'PUT',
1154
+ headers: {
1155
+ 'Content-Type': 'application/json',
1156
+ },
1157
+ body: JSON.stringify({ full_name: fullName || null })
1158
+ });
1159
+
1160
+ if (!response.ok) {
1161
+ const error = await response.json();
1162
+ throw new Error(error.detail || 'Failed to update profile');
1163
+ }
1164
+
1165
+ const updatedUser = await response.json();
1166
+ currentUser = updatedUser;
1167
+
1168
+ // Update display name in header
1169
+ const displayName = currentUser.full_name || currentUser.username;
1170
+ document.getElementById('username-display').textContent = displayName;
1171
+
1172
+ showToast('Profile updated successfully', 'success');
1173
+ } catch (error) {
1174
+ showToast(error.message, 'error');
1175
+ }
1176
+ }
1177
+
1178
+ async function handlePasswordChange(e) {
1179
+ e.preventDefault();
1180
+
1181
+ const currentPassword = document.getElementById('current-password').value;
1182
+ const newPassword = document.getElementById('new-password').value;
1183
+ const confirmPassword = document.getElementById('confirm-password').value;
1184
+
1185
+ if (newPassword !== confirmPassword) {
1186
+ showToast('New passwords do not match', 'error');
1187
+ return;
1188
+ }
1189
+
1190
+ if (newPassword.length < 6) {
1191
+ showToast('Password must be at least 6 characters', 'error');
1192
+ return;
1193
+ }
1194
+
1195
+ try {
1196
+ const response = await fetchWithAuth(`${API_BASE}/auth/password`, {
1197
+ method: 'PUT',
1198
+ headers: {
1199
+ 'Content-Type': 'application/json',
1200
+ },
1201
+ body: JSON.stringify({
1202
+ current_password: currentPassword,
1203
+ new_password: newPassword
1204
+ })
1205
+ });
1206
+
1207
+ if (!response.ok) {
1208
+ const error = await response.json();
1209
+ throw new Error(error.detail || 'Failed to change password');
1210
+ }
1211
+
1212
+ document.getElementById('password-form').reset();
1213
+ showToast('Password changed successfully', 'success');
1214
+ } catch (error) {
1215
+ showToast(error.message, 'error');
1216
+ }
1217
+ }
1218
+
1219
+ async function handleApiKeyUpdate(e) {
1220
+ e.preventDefault();
1221
+
1222
+ const apiKey = document.getElementById('api-key').value.trim();
1223
+
1224
+ if (!apiKey) {
1225
+ showToast('Please enter an API key', 'error');
1226
+ return;
1227
+ }
1228
+
1229
+ if (!apiKey.startsWith('sk-ant-')) {
1230
+ showToast('Invalid API key format. Anthropic API keys start with sk-ant-', 'error');
1231
+ return;
1232
+ }
1233
+
1234
+ try {
1235
+ const response = await fetchWithAuth(`${API_BASE}/auth/api-key`, {
1236
+ method: 'PUT',
1237
+ headers: {
1238
+ 'Content-Type': 'application/json',
1239
+ },
1240
+ body: JSON.stringify({ api_key: apiKey })
1241
+ });
1242
+
1243
+ if (!response.ok) {
1244
+ const error = await response.json();
1245
+ throw new Error(error.detail || 'Failed to update API key');
1246
+ }
1247
+
1248
+ currentUser.has_api_key = true;
1249
+ updateApiKeyStatus(true);
1250
+ document.getElementById('api-key').value = '';
1251
+ showToast('API key saved successfully', 'success');
1252
+ } catch (error) {
1253
+ showToast(error.message, 'error');
1254
+ }
1255
+ }
1256
+
1257
+ async function handleApiKeyDelete() {
1258
+ if (!confirm('Are you sure you want to delete your API key? Wine label scanning will use the default system key if available.')) {
1259
+ return;
1260
+ }
1261
+
1262
+ try {
1263
+ const response = await fetchWithAuth(`${API_BASE}/auth/api-key`, {
1264
+ method: 'DELETE'
1265
+ });
1266
+
1267
+ if (!response.ok) {
1268
+ const error = await response.json();
1269
+ throw new Error(error.detail || 'Failed to delete API key');
1270
+ }
1271
+
1272
+ currentUser.has_api_key = false;
1273
+ updateApiKeyStatus(false);
1274
+ showToast('API key deleted successfully', 'success');
1275
+ } catch (error) {
1276
+ showToast(error.message, 'error');
1277
+ }
1278
+ }