winebox 0.1.3__py3-none-any.whl → 0.1.5__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/__init__.py +1 -1
- winebox/config.py +36 -5
- winebox/main.py +48 -1
- winebox/models/user.py +2 -0
- winebox/routers/auth.py +117 -3
- winebox/routers/wines.py +130 -71
- winebox/services/image_storage.py +138 -9
- winebox/services/vision.py +50 -23
- winebox/static/css/style.css +201 -0
- winebox/static/index.html +176 -19
- winebox/static/js/app.js +343 -62
- {winebox-0.1.3.dist-info → winebox-0.1.5.dist-info}/METADATA +4 -1
- {winebox-0.1.3.dist-info → winebox-0.1.5.dist-info}/RECORD +16 -16
- {winebox-0.1.3.dist-info → winebox-0.1.5.dist-info}/WHEEL +0 -0
- {winebox-0.1.3.dist-info → winebox-0.1.5.dist-info}/entry_points.txt +0 -0
- {winebox-0.1.3.dist-info → winebox-0.1.5.dist-info}/licenses/LICENSE +0 -0
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', () => {
|
|
@@ -42,11 +43,21 @@ function initAuth() {
|
|
|
42
43
|
// Logout button
|
|
43
44
|
document.getElementById('logout-btn').addEventListener('click', handleLogout);
|
|
44
45
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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"]');
|
|
50
61
|
const eyeIcon = this.querySelector('.eye-icon');
|
|
51
62
|
const eyeOffIcon = this.querySelector('.eye-off-icon');
|
|
52
63
|
|
|
@@ -62,7 +73,7 @@ function initAuth() {
|
|
|
62
73
|
this.setAttribute('aria-label', 'Show password');
|
|
63
74
|
}
|
|
64
75
|
});
|
|
65
|
-
}
|
|
76
|
+
});
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
async function checkAuth() {
|
|
@@ -95,7 +106,9 @@ function showMainApp() {
|
|
|
95
106
|
document.body.classList.remove('logged-out');
|
|
96
107
|
document.getElementById('page-login').classList.remove('active');
|
|
97
108
|
document.getElementById('user-info').style.display = 'flex';
|
|
98
|
-
|
|
109
|
+
// Display full name if available, otherwise username
|
|
110
|
+
const displayName = currentUser.full_name || currentUser.username;
|
|
111
|
+
document.getElementById('username-display').textContent = displayName;
|
|
99
112
|
loadDashboard();
|
|
100
113
|
}
|
|
101
114
|
|
|
@@ -203,6 +216,9 @@ function navigateTo(page) {
|
|
|
203
216
|
case 'search':
|
|
204
217
|
// Search results loaded on form submit
|
|
205
218
|
break;
|
|
219
|
+
case 'settings':
|
|
220
|
+
loadSettings();
|
|
221
|
+
break;
|
|
206
222
|
}
|
|
207
223
|
}
|
|
208
224
|
|
|
@@ -215,6 +231,7 @@ function initForms() {
|
|
|
215
231
|
document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
|
|
216
232
|
document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
|
|
217
233
|
clearRawLabelText();
|
|
234
|
+
lastScanResult = null; // Clear stored scan result
|
|
218
235
|
});
|
|
219
236
|
|
|
220
237
|
// Image previews - make clickable to trigger file input
|
|
@@ -271,6 +288,12 @@ function initForms() {
|
|
|
271
288
|
|
|
272
289
|
// History filter
|
|
273
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);
|
|
274
297
|
}
|
|
275
298
|
|
|
276
299
|
function previewImage(input, previewId) {
|
|
@@ -317,6 +340,7 @@ async function scanLabels() {
|
|
|
317
340
|
}
|
|
318
341
|
|
|
319
342
|
const result = await response.json();
|
|
343
|
+
lastScanResult = result; // Store for checkin
|
|
320
344
|
populateFormFromScan(result);
|
|
321
345
|
const methodName = result.method === 'claude_vision' ? 'Claude Vision' : 'Tesseract OCR';
|
|
322
346
|
showToast(`Label scanned with ${methodName}`, 'success');
|
|
@@ -421,70 +445,86 @@ function showScanningIndicator(show) {
|
|
|
421
445
|
}
|
|
422
446
|
}
|
|
423
447
|
|
|
424
|
-
|
|
448
|
+
// Store pending checkin data for confirmation
|
|
449
|
+
let pendingCheckinData = null;
|
|
450
|
+
|
|
451
|
+
function handleCheckin(e) {
|
|
425
452
|
e.preventDefault();
|
|
426
|
-
const form = e.target;
|
|
427
|
-
const formData = new FormData(form);
|
|
428
453
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
+
}
|
|
434
459
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
};
|
|
439
476
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
form.reset();
|
|
443
|
-
} catch (error) {
|
|
444
|
-
showToast(error.message, 'error');
|
|
445
|
-
}
|
|
477
|
+
// Show the confirmation modal with editable fields
|
|
478
|
+
showCheckinConfirmation();
|
|
446
479
|
}
|
|
447
480
|
|
|
448
|
-
function showCheckinConfirmation(
|
|
481
|
+
function showCheckinConfirmation() {
|
|
449
482
|
const modal = document.getElementById('checkin-confirm-modal');
|
|
483
|
+
const data = pendingCheckinData;
|
|
450
484
|
|
|
451
|
-
// Set
|
|
452
|
-
document.getElementById('checkin-confirm-name').textContent = wine.name;
|
|
453
|
-
|
|
454
|
-
// Set image
|
|
485
|
+
// Set image preview
|
|
455
486
|
const imageContainer = document.getElementById('checkin-confirm-image');
|
|
456
|
-
if (
|
|
457
|
-
|
|
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);
|
|
458
493
|
} else {
|
|
459
494
|
imageContainer.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);">No image</div>';
|
|
460
495
|
}
|
|
461
496
|
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
+
}
|
|
483
523
|
|
|
484
524
|
const backOcrSection = document.getElementById('checkin-confirm-back-ocr-section');
|
|
485
|
-
if (
|
|
525
|
+
if (data.backLabelText) {
|
|
486
526
|
backOcrSection.style.display = 'block';
|
|
487
|
-
document.getElementById('checkin-confirm-back-ocr').textContent =
|
|
527
|
+
document.getElementById('checkin-confirm-back-ocr').textContent = data.backLabelText;
|
|
488
528
|
} else {
|
|
489
529
|
backOcrSection.style.display = 'none';
|
|
490
530
|
}
|
|
@@ -492,18 +532,91 @@ function showCheckinConfirmation(wine) {
|
|
|
492
532
|
// Show modal
|
|
493
533
|
modal.classList.add('active');
|
|
494
534
|
|
|
495
|
-
// Set up
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
}
|
|
499
547
|
};
|
|
500
548
|
|
|
501
|
-
|
|
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
|
+
}
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const response = await fetchWithAuth(`${API_BASE}/wines/checkin`, {
|
|
588
|
+
method: 'POST',
|
|
589
|
+
body: formData
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
if (!response.ok) {
|
|
593
|
+
const error = await response.json();
|
|
594
|
+
throw new Error(error.detail || 'Check-in failed');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const wine = await response.json();
|
|
598
|
+
showToast(`Successfully checked in: ${wine.name}`, 'success');
|
|
599
|
+
|
|
600
|
+
// Close modal and reset form
|
|
502
601
|
modal.classList.remove('active');
|
|
503
|
-
|
|
602
|
+
document.getElementById('checkin-form').reset();
|
|
504
603
|
document.getElementById('front-preview').innerHTML = 'Tap to take photo or select image';
|
|
505
604
|
document.getElementById('back-preview').innerHTML = 'Tap to take photo or select image';
|
|
506
|
-
|
|
605
|
+
clearRawLabelText();
|
|
606
|
+
lastScanResult = null;
|
|
607
|
+
pendingCheckinData = null;
|
|
608
|
+
|
|
609
|
+
// Navigate to cellar
|
|
610
|
+
navigateTo('cellar');
|
|
611
|
+
} catch (error) {
|
|
612
|
+
showToast(error.message, 'error');
|
|
613
|
+
}
|
|
614
|
+
}
|
|
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
|
|
507
620
|
}
|
|
508
621
|
|
|
509
622
|
async function handleSearch(e) {
|
|
@@ -995,3 +1108,171 @@ function debounce(func, wait) {
|
|
|
995
1108
|
timeout = setTimeout(later, wait);
|
|
996
1109
|
};
|
|
997
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
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: winebox
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Wine Cellar Management Application with OCR label scanning
|
|
5
5
|
Project-URL: Homepage, https://github.com/jdrumgoole/winebox
|
|
6
6
|
Project-URL: Repository, https://github.com/jdrumgoole/winebox
|
|
@@ -33,6 +33,7 @@ Requires-Dist: pydantic>=2.0.0
|
|
|
33
33
|
Requires-Dist: pytesseract>=0.3.10
|
|
34
34
|
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
35
35
|
Requires-Dist: python-multipart>=0.0.6
|
|
36
|
+
Requires-Dist: slowapi>=0.1.9
|
|
36
37
|
Requires-Dist: sqlalchemy>=2.0.0
|
|
37
38
|
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
38
39
|
Provides-Extra: dev
|
|
@@ -41,6 +42,8 @@ Requires-Dist: httpx>=0.26.0; extra == 'dev'
|
|
|
41
42
|
Requires-Dist: invoke>=2.2.0; extra == 'dev'
|
|
42
43
|
Requires-Dist: myst-parser>=2.0.0; extra == 'dev'
|
|
43
44
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest-playwright>=0.4.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest-xdist>=3.5.0; extra == 'dev'
|
|
44
47
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
45
48
|
Requires-Dist: sphinx>=7.0.0; extra == 'dev'
|
|
46
49
|
Description-Content-Type: text/markdown
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
winebox/__init__.py,sha256=
|
|
2
|
-
winebox/config.py,sha256=
|
|
1
|
+
winebox/__init__.py,sha256=ESGS6HomBT7Xl256jY5X0p4xKi9lh7XlrhskSQosf2U,75
|
|
2
|
+
winebox/config.py,sha256=pfwXrKsI4KsXtA4lehEhAc9xfjBGu1OzdhiUq4mixto,2438
|
|
3
3
|
winebox/database.py,sha256=jTdf9mb48D8644THo6Mx6lHCKDFlAZ18P8Dvuk2eROI,1128
|
|
4
|
-
winebox/main.py,sha256=
|
|
4
|
+
winebox/main.py,sha256=RCB1oCxWbKXiMtQvfLabhGI_xvDjr_pGLEYeo2SSgeU,4104
|
|
5
5
|
winebox/cli/__init__.py,sha256=FOUoclklBXVfsfON3ohA8v_coJ9p5LSbVYvTucyXieg,29
|
|
6
6
|
winebox/cli/server.py,sha256=aN4znSvQRCZFZlQ1gcGag7OCFngwzUJdi084ZdNrnNI,8416
|
|
7
7
|
winebox/cli/user_admin.py,sha256=xwkd4-YN9uL6hX7-AuiDUNekG7x45l1lKor7AsOUxds,8549
|
|
8
8
|
winebox/models/__init__.py,sha256=2avQWHx53-5Z6jnzFlhh5u10Dw99w7en2nqCXEbwqRI,312
|
|
9
9
|
winebox/models/inventory.py,sha256=VLxpEIdYbD-oG9OfIGkHKDKm-cbSSo_aZGx66wRqR7Y,1324
|
|
10
10
|
winebox/models/transaction.py,sha256=9k1AAK0unYNseDubTaiFnGUGDVtuOqrjNUjIDukFDbc,1812
|
|
11
|
-
winebox/models/user.py,sha256=
|
|
11
|
+
winebox/models/user.py,sha256=Ke7DJsVtUzHlKQUiHEDHTi4Rm-tqCbrin4iZyt4SP9A,1734
|
|
12
12
|
winebox/models/wine.py,sha256=R40TTRunPcTG4_D8kEWhopM2k6ne_awET7ctY4fk-HE,2503
|
|
13
13
|
winebox/routers/__init__.py,sha256=TEjBLPCkG007ItXMONZ2TsjOxl_TvHis1v84IOkXm1A,167
|
|
14
|
-
winebox/routers/auth.py,sha256=
|
|
14
|
+
winebox/routers/auth.py,sha256=0F8tXJSo_oIYEY25dO2QZ-NtuF6-Fi4HnSLTCa6TWzs,5657
|
|
15
15
|
winebox/routers/cellar.py,sha256=QSyAnPQ2hiNaoojLk-5hJy95OnG8RMHOD0h5LV6bAWM,3404
|
|
16
16
|
winebox/routers/search.py,sha256=aIqwsjnB9e-D84VVcuye-01V_9WBUjFKzXNsX0gkRak,4971
|
|
17
17
|
winebox/routers/transactions.py,sha256=K0UDIJbVSTjV7NdI_CS0pvoTflH3Yp89DmneyFyl9ok,2055
|
|
18
|
-
winebox/routers/wines.py,sha256=
|
|
18
|
+
winebox/routers/wines.py,sha256=szr9h7W2Q-Xxn8LYd6rXtEOVWST7hthXGrZ84xSyLpY,16948
|
|
19
19
|
winebox/schemas/__init__.py,sha256=R4j1wdV1F-6kA45rHA8NUP2cqxBmLb2CMklo8tFjzGQ,357
|
|
20
20
|
winebox/schemas/transaction.py,sha256=GV_AZGeuFp_nOkg902UUhrHGwB1DxcQmGoG82m1UccI,899
|
|
21
21
|
winebox/schemas/wine.py,sha256=xHWDiV5bvEJuLPN2qWgOhqVMOTdlDSmfan5mtEy3Nvw,2301
|
|
22
22
|
winebox/services/__init__.py,sha256=rlAd5FBcqZJ7rsrFqR8MwTqGJVerlQHVUleEmoIaz1Y,277
|
|
23
23
|
winebox/services/auth.py,sha256=RDXP26hL3cGGxKcniXWMZ0sPUP6ew4LLMnfo4dv4P2E,3854
|
|
24
|
-
winebox/services/image_storage.py,sha256=
|
|
24
|
+
winebox/services/image_storage.py,sha256=r1QzOFY0KzcJVdQV2fa4df9GtV3T7weQB17oEQZLTTo,6306
|
|
25
25
|
winebox/services/ocr.py,sha256=zFmQGj3a_jv21MBx8RfNkFIenAOnJ0O28JupSrsNC44,4925
|
|
26
|
-
winebox/services/vision.py,sha256=
|
|
26
|
+
winebox/services/vision.py,sha256=jINJ0RmmIkBhiuAyYAUCtmhxMWTz1pNcTSOUck-xjPg,10263
|
|
27
27
|
winebox/services/wine_parser.py,sha256=n5ldv2x2XsTfpK3acYDyMNyZHdJ_8WQU0keOql14MJ0,10456
|
|
28
28
|
winebox/static/favicon.svg,sha256=jYFlQ-dEfhUoHQJepoSEMYQx0jxk8HyuX0wrd4OApzE,1061
|
|
29
|
-
winebox/static/index.html,sha256=
|
|
30
|
-
winebox/static/css/style.css,sha256=
|
|
31
|
-
winebox/static/js/app.js,sha256=
|
|
32
|
-
winebox-0.1.
|
|
33
|
-
winebox-0.1.
|
|
34
|
-
winebox-0.1.
|
|
35
|
-
winebox-0.1.
|
|
36
|
-
winebox-0.1.
|
|
29
|
+
winebox/static/index.html,sha256=LZKro_4jyebGXCPvP87TbBrKuVh_Nma7n9JRhUGwh2Q,27473
|
|
30
|
+
winebox/static/css/style.css,sha256=QEcmosNtf4_oH7JGYveQYRAzHdS5vh93fTVdyBA9YhM,29987
|
|
31
|
+
winebox/static/js/app.js,sha256=0TVu8WFHZhHt-jhFNFetAHCTn89xLc8exf99GjtFRD4,45251
|
|
32
|
+
winebox-0.1.5.dist-info/METADATA,sha256=kSNMBqDN-ut8IouWmP7cyke4694VrpLQ1S9aB-GsBzY,8138
|
|
33
|
+
winebox-0.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
34
|
+
winebox-0.1.5.dist-info/entry_points.txt,sha256=XY-GMf023m8Iof3rmokkITONECm8gCOVkTX0rCkze30,103
|
|
35
|
+
winebox-0.1.5.dist-info/licenses/LICENSE,sha256=3popbhCShFhWVwTLKeQE-JNDYuPOYfmCiz-LoQpizTA,1070
|
|
36
|
+
winebox-0.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|