focustiki commited on
Commit
98bc933
·
1 Parent(s): c257b79

feat: dark theme, hover text fixes, login RLS token, AI provider fallback, status cards rendering, optional cutoff, improved error visibility

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .streamlit/secrets.toml +15 -0
  2. .streamlit/secrets_backup_20250923_195857.toml +15 -0
  3. COLOR_SCHEME_UPDATE.md +85 -0
  4. DEPLOYMENT_QUICK_REFERENCE.md +157 -0
  5. UI_IMPROVEMENTS_GUIDE.md +287 -0
  6. __pycache__/streamlit_app.cpython-310.pyc +0 -0
  7. __pycache__/streamlit_app_modern.cpython-310.pyc +0 -0
  8. __pycache__/ui_improvements.cpython-310.pyc +0 -0
  9. backups/deployment_log_20250923_202620.json +62 -0
  10. backups/deployment_log_20250923_203633.json +62 -0
  11. backups/deployment_log_20250923_204101.json +1 -0
  12. backups/deployment_log_20250923_204113.json +14 -0
  13. backups/deployment_log_20250923_205215.json +62 -0
  14. backups/deployment_log_20250924_092715.json +1 -0
  15. backups/hero_section_html_fix_20250923_203834/requirements.txt +9 -0
  16. backups/hero_section_html_fix_20250923_203834/secrets.toml +15 -0
  17. backups/hero_section_html_fix_20250923_203834/streamlit_app.py +891 -0
  18. backups/indentation_fix_20250923_203937/requirements.txt +9 -0
  19. backups/indentation_fix_20250923_203937/secrets.toml +15 -0
  20. backups/indentation_fix_20250923_203937/streamlit_app.py +891 -0
  21. backups/pre_modern_deployment_20250923_202615/backup_metadata.json +32 -0
  22. backups/pre_modern_deployment_20250923_202615/requirements.txt +9 -0
  23. backups/pre_modern_deployment_20250923_202615/secrets.toml +15 -0
  24. backups/pre_modern_deployment_20250923_202615/streamlit_app.py +612 -0
  25. backups/pre_modern_deployment_20250923_202615/streamlit_app_modern.py +891 -0
  26. backups/pre_modern_deployment_20250923_202615/test_app.py +233 -0
  27. backups/pre_modern_deployment_20250923_202615/ui_improvements.py +494 -0
  28. backups/pre_modern_deployment_20250923_203628/backup_metadata.json +34 -0
  29. backups/pre_modern_deployment_20250923_203628/requirements.txt +9 -0
  30. backups/pre_modern_deployment_20250923_203628/secrets.toml +15 -0
  31. backups/pre_modern_deployment_20250923_203628/streamlit_app.py +891 -0
  32. backups/pre_modern_deployment_20250923_203628/streamlit_app_modern.py +891 -0
  33. backups/pre_modern_deployment_20250923_203628/test_app.py +233 -0
  34. backups/pre_modern_deployment_20250923_203628/ui_improvements.py +518 -0
  35. backups/pre_modern_deployment_20250923_205210/backup_metadata.json +35 -0
  36. backups/pre_modern_deployment_20250923_205210/requirements.txt +9 -0
  37. backups/pre_modern_deployment_20250923_205210/secrets.toml +15 -0
  38. backups/pre_modern_deployment_20250923_205210/streamlit_app.py +890 -0
  39. backups/pre_modern_deployment_20250923_205210/streamlit_app_modern.py +891 -0
  40. backups/pre_modern_deployment_20250923_205210/test_app.py +233 -0
  41. backups/pre_modern_deployment_20250923_205210/ui_improvements.py +583 -0
  42. backups/pre_test_backup_20250923_202554/requirements.txt +9 -0
  43. backups/pre_test_backup_20250923_202554/secrets.toml +15 -0
  44. backups/pre_test_backup_20250923_202554/streamlit_app.py +612 -0
  45. backups/pre_test_backup_20250923_202613/requirements.txt +9 -0
  46. backups/pre_test_backup_20250923_202613/secrets.toml +15 -0
  47. backups/pre_test_backup_20250923_202613/streamlit_app.py +612 -0
  48. backups/pre_test_backup_20250923_202627/requirements.txt +9 -0
  49. backups/pre_test_backup_20250923_202627/secrets.toml +15 -0
  50. backups/pre_test_backup_20250923_202627/streamlit_app.py +891 -0
.streamlit/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
.streamlit/secrets_backup_20250923_195857.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
COLOR_SCHEME_UPDATE.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Care Count - Laurier University Color Scheme Update
2
+
3
+ ## 🎨 Color Scheme Changes
4
+
5
+ The Care Count app has been updated to match the Wilfrid Laurier University website color scheme for a more professional and cohesive brand experience.
6
+
7
+ ### Updated Color Palette
8
+
9
+ | Element | Old Color | New Color | Description |
10
+ |---------|-----------|-----------|-------------|
11
+ | **Primary Purple** | `#6d28d9` | `#6b46c1` | More vibrant Laurier purple |
12
+ | **Primary Gold** | `#fde047` | `#fbbf24` | Warmer, more professional gold |
13
+ | **Background** | `#0b1420` (dark) | `#f9fafb` (light gray) | Clean, light background |
14
+ | **Secondary Background** | `#0f1a2a` (dark) | `#312e81` (purple) | Subtle purple accent |
15
+ | **Card Background** | Dark theme | White | Clean, professional cards |
16
+ | **Text Color** | `#f3f4f6` (light) | `#1f2937` (dark) | High contrast dark text |
17
+
18
+ ### Design Changes
19
+
20
+ #### 1. **Hero Section**
21
+ - **Background**: Solid Laurier purple (`#6b46c1`)
22
+ - **Layout**: Left-aligned text (matching Laurier website)
23
+ - **Typography**: Larger, more prominent headings
24
+ - **Effects**: Subtle gradient overlay for depth
25
+
26
+ #### 2. **Cards & Components**
27
+ - **Background**: Clean white cards with subtle shadows
28
+ - **Borders**: Light gray borders with purple hover effects
29
+ - **Hover Effects**: Subtle lift animation with purple accent
30
+
31
+ #### 3. **Status Cards**
32
+ - **Background**: White with light shadows
33
+ - **Values**: Purple text for metrics
34
+ - **Labels**: Dark gray for better readability
35
+
36
+ #### 4. **Form Elements**
37
+ - **Inputs**: White background with light gray borders
38
+ - **Focus States**: Purple border with subtle glow
39
+ - **Buttons**: Laurier purple with white text
40
+
41
+ #### 5. **Overall Layout**
42
+ - **Background**: Light gray (`#f9fafb`) for better readability
43
+ - **Typography**: Dark text for high contrast
44
+ - **Spacing**: Consistent with Laurier design principles
45
+
46
+ ### Benefits of the New Color Scheme
47
+
48
+ 1. **Brand Consistency**: Matches Laurier University's official colors
49
+ 2. **Better Readability**: Light background with dark text improves accessibility
50
+ 3. **Professional Appearance**: Clean, modern design that builds trust
51
+ 4. **Improved Contrast**: Better visibility for all users
52
+ 5. **Mobile Friendly**: Light theme works better on mobile devices
53
+
54
+ ### Technical Implementation
55
+
56
+ The color scheme is implemented using CSS custom properties (variables) in the `ui_improvements.py` file:
57
+
58
+ ```css
59
+ :root {
60
+ --primary-purple: #6b46c1; /* Laurier purple */
61
+ --primary-gold: #fbbf24; /* Laurier gold */
62
+ --primary-dark: #1e1b4b; /* Deep purple background */
63
+ --secondary-dark: #312e81; /* Slightly lighter purple */
64
+ /* ... other colors */
65
+ }
66
+ ```
67
+
68
+ ### Accessibility Improvements
69
+
70
+ - **High Contrast**: Dark text on light background meets WCAG guidelines
71
+ - **Focus States**: Clear purple focus indicators
72
+ - **Color Independence**: Information not conveyed by color alone
73
+ - **Readable Typography**: Improved font weights and sizes
74
+
75
+ ### Future Considerations
76
+
77
+ The new color scheme provides a solid foundation for:
78
+ - **Dark Mode Toggle**: Easy to implement with CSS variables
79
+ - **Brand Customization**: Colors can be easily adjusted
80
+ - **Seasonal Themes**: Framework supports theme variations
81
+ - **Accessibility**: Built-in support for high contrast modes
82
+
83
+ ---
84
+
85
+ **Result**: The Care Count app now has a professional, Laurier-branded appearance that matches the university's website while maintaining excellent usability and accessibility.
DEPLOYMENT_QUICK_REFERENCE.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Care Count - Deployment Quick Reference
2
+
3
+ ## 🚀 Quick Commands
4
+
5
+ ### Start the App
6
+ ```bash
7
+ streamlit run streamlit_app.py
8
+ ```
9
+ **Access at:** http://localhost:8501
10
+
11
+ ### Test the App
12
+ ```bash
13
+ # Run all tests
14
+ python test_app.py test
15
+
16
+ # Create backup
17
+ python test_app.py backup "description"
18
+
19
+ # Restore from backup
20
+ python test_app.py restore backup_name
21
+ ```
22
+
23
+ ### Deploy Modern UI
24
+ ```bash
25
+ # Deploy with testing
26
+ python deploy_modern.py deploy
27
+
28
+ # List backups
29
+ python deploy_modern.py list
30
+
31
+ # Rollback
32
+ python deploy_modern.py rollback backup_name
33
+
34
+ # Test only
35
+ python deploy_modern.py test
36
+ ```
37
+
38
+ ## 📁 Important Files
39
+
40
+ | File | Purpose |
41
+ |------|---------|
42
+ | `streamlit_app.py` | Main app (now modern version) |
43
+ | `streamlit_app_modern.py` | Modern UI source |
44
+ | `ui_improvements.py` | UI component library |
45
+ | `test_app.py` | Testing framework |
46
+ | `deploy_modern.py` | Deployment script |
47
+ | `.streamlit/secrets.toml` | Configuration |
48
+ | `backups/` | Automatic backups |
49
+
50
+ ## 🔧 Configuration
51
+
52
+ ### Required Secrets
53
+ ```toml
54
+ # .streamlit/secrets.toml
55
+ SUPABASE_URL = "https://your-project.supabase.co"
56
+ SUPABASE_KEY = "your-anon-key"
57
+ ```
58
+
59
+ ### Optional Configuration
60
+ ```toml
61
+ APP_TZ = "America/Toronto"
62
+ CUTOFF_HOUR = "20"
63
+ INACTIVITY_MIN = "30"
64
+ PROVIDER = "nebius"
65
+ GEMMA_MODEL = "google/gemma-3-27b-it"
66
+ ```
67
+
68
+ ## 🧪 Testing Checklist
69
+
70
+ - [ ] App starts without errors
71
+ - [ ] Authentication flow works
72
+ - [ ] Visit management functions
73
+ - [ ] Item identification works
74
+ - [ ] Item logging functions
75
+ - [ ] Responsive design works
76
+ - [ ] All buttons and forms work
77
+
78
+ ## 🚨 Troubleshooting
79
+
80
+ ### App Won't Start
81
+ 1. Check if port 8501 is free
82
+ 2. Verify all dependencies installed
83
+ 3. Check secrets configuration
84
+ 4. Review error logs
85
+
86
+ ### Authentication Issues
87
+ 1. Verify Supabase URL and key
88
+ 2. Check network connectivity
89
+ 3. Review OTP email delivery
90
+ 4. Check database permissions
91
+
92
+ ### UI Issues
93
+ 1. Clear browser cache
94
+ 2. Check for JavaScript errors
95
+ 3. Verify CSS loading
96
+ 4. Test on different browsers
97
+
98
+ ### Rollback Process
99
+ 1. List available backups: `python deploy_modern.py list`
100
+ 2. Rollback to previous version: `python deploy_modern.py rollback backup_name`
101
+ 3. Verify app functionality
102
+ 4. Check logs for issues
103
+
104
+ ## 📊 Monitoring
105
+
106
+ ### Log Files
107
+ - `care_count.log` - Application logs
108
+ - `test_results.log` - Test results
109
+ - `deployment.log` - Deployment logs
110
+ - `backups/deployment_log_*.json` - Deployment history
111
+
112
+ ### Health Checks
113
+ - App accessible at http://localhost:8501
114
+ - All UI elements loading
115
+ - Database connectivity working
116
+ - AI services responding
117
+
118
+ ## 🔄 Maintenance
119
+
120
+ ### Daily
121
+ - Check app accessibility
122
+ - Review error logs
123
+ - Monitor performance
124
+
125
+ ### Weekly
126
+ - Run comprehensive tests
127
+ - Review user feedback
128
+ - Check backup integrity
129
+
130
+ ### Monthly
131
+ - Update dependencies
132
+ - Review security logs
133
+ - Performance optimization
134
+
135
+ ## 📞 Emergency Procedures
136
+
137
+ ### App Down
138
+ 1. Check if process is running
139
+ 2. Review error logs
140
+ 3. Restart app: `streamlit run streamlit_app.py`
141
+ 4. If issues persist, rollback to last working version
142
+
143
+ ### Data Issues
144
+ 1. Check database connectivity
145
+ 2. Verify Supabase configuration
146
+ 3. Review data integrity
147
+ 4. Contact database administrator
148
+
149
+ ### Security Concerns
150
+ 1. Review access logs
151
+ 2. Check for unauthorized access
152
+ 3. Verify authentication flow
153
+ 4. Update credentials if needed
154
+
155
+ ---
156
+
157
+ **Remember:** Always create a backup before making changes!
UI_IMPROVEMENTS_GUIDE.md ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Care Count - Modern UI/UX Improvements Guide
2
+
3
+ ## 🎯 Overview
4
+
5
+ This guide documents the comprehensive UI/UX improvements made to the Care Count application, bringing it to industry-standard quality while maintaining all backend functionality.
6
+
7
+ ## 🚀 What's New
8
+
9
+ ### 1. Modern Design System
10
+ - **Industry-standard color palette** with Laurier theme (purple & gold)
11
+ - **Consistent typography** using Inter font family
12
+ - **Responsive design** that works on all devices
13
+ - **Accessibility improvements** with proper focus states and ARIA labels
14
+ - **Modern shadows and animations** for better visual hierarchy
15
+
16
+ ### 2. Enhanced User Experience
17
+ - **Improved authentication flow** with better error handling
18
+ - **Modern form components** with better validation
19
+ - **Enhanced status cards** with real-time updates
20
+ - **Better visual feedback** for all user actions
21
+ - **Improved loading states** and progress indicators
22
+
23
+ ### 3. Professional UI Components
24
+ - **Modern cards and containers** with hover effects
25
+ - **Enhanced buttons** with proper states (primary, secondary, danger)
26
+ - **Better form inputs** with improved styling
27
+ - **Professional badges** for status indicators
28
+ - **Responsive grid layouts** for better organization
29
+
30
+ ## 📁 File Structure
31
+
32
+ ```
33
+ care-count-starter/
34
+ ├── streamlit_app.py # Original app (backed up)
35
+ ├── streamlit_app_modern.py # Modern UI version
36
+ ├── ui_improvements.py # UI component library
37
+ ├── test_app.py # Testing framework
38
+ ├── deploy_modern.py # Deployment script
39
+ ├── backups/ # Automatic backups
40
+ │ ├── backup_YYYYMMDD_HHMMSS/
41
+ │ └── deployment_log_*.json
42
+ └── UI_IMPROVEMENTS_GUIDE.md # This guide
43
+ ```
44
+
45
+ ## 🛠️ Key Improvements
46
+
47
+ ### Authentication Flow
48
+ - **Modern hero section** with gradient background
49
+ - **Better form validation** with clear error messages
50
+ - **Enhanced OTP verification** with improved UX
51
+ - **Professional loading states** during authentication
52
+
53
+ ### Dashboard
54
+ - **Modern status cards** with hover effects
55
+ - **Real-time metrics** with better visual presentation
56
+ - **Responsive grid layout** that adapts to screen size
57
+ - **Professional typography** with proper hierarchy
58
+
59
+ ### Visit Management
60
+ - **Enhanced visit tracking** with better visual feedback
61
+ - **Modern button styling** with proper states
62
+ - **Improved form layouts** with better spacing
63
+ - **Professional status indicators**
64
+
65
+ ### Item Identification
66
+ - **Better camera interface** with improved styling
67
+ - **Enhanced file upload** with drag-and-drop styling
68
+ - **Modern AI feedback** with processing indicators
69
+ - **Professional result display**
70
+
71
+ ### Item Logging
72
+ - **Improved form design** with better organization
73
+ - **Enhanced validation** with clear error messages
74
+ - **Modern input styling** with focus states
75
+ - **Professional success/error feedback**
76
+
77
+ ## 🧪 Testing & Quality Assurance
78
+
79
+ ### Automated Testing
80
+ - **Comprehensive test suite** (`test_app.py`)
81
+ - **UI element validation** to ensure all components work
82
+ - **Performance testing** for load times and responsiveness
83
+ - **Accessibility testing** for proper focus management
84
+
85
+ ### Safe Deployment
86
+ - **Automatic backups** before any changes
87
+ - **Rollback capability** to previous versions
88
+ - **Deployment logging** for audit trails
89
+ - **Error handling** with graceful fallbacks
90
+
91
+ ## 🔧 Usage Instructions
92
+
93
+ ### Running the Modern App
94
+ ```bash
95
+ # The modern app is now the default
96
+ streamlit run streamlit_app.py
97
+ ```
98
+
99
+ ### Testing the App
100
+ ```bash
101
+ # Run comprehensive tests
102
+ python test_app.py test
103
+
104
+ # Create a backup
105
+ python test_app.py backup "description"
106
+
107
+ # Restore from backup
108
+ python test_app.py restore backup_name
109
+ ```
110
+
111
+ ### Deployment Management
112
+ ```bash
113
+ # Deploy modern UI (with testing)
114
+ python deploy_modern.py deploy
115
+
116
+ # List available backups
117
+ python deploy_modern.py list
118
+
119
+ # Rollback to previous version
120
+ python deploy_modern.py rollback backup_name
121
+
122
+ # Run tests only
123
+ python deploy_modern.py test
124
+ ```
125
+
126
+ ## 🎨 Design System
127
+
128
+ ### Color Palette
129
+ - **Primary Purple**: `#6d28d9` - Main brand color
130
+ - **Primary Gold**: `#fde047` - Accent color
131
+ - **Dark Background**: `#0b1420` - Main background
132
+ - **Secondary Dark**: `#0f1a2a` - Card backgrounds
133
+ - **Accent Blue**: `#3b82f6` - Information elements
134
+ - **Success Green**: `#10b981` - Success states
135
+ - **Warning Orange**: `#f59e0b` - Warning states
136
+ - **Error Red**: `#ef4444` - Error states
137
+
138
+ ### Typography
139
+ - **Font Family**: Inter (system fallbacks)
140
+ - **Headings**: 700 weight, -0.025em letter spacing
141
+ - **Body Text**: 400 weight, 1.6 line height
142
+ - **Small Text**: 0.75rem for captions
143
+
144
+ ### Spacing System
145
+ - **Base Unit**: 0.25rem (4px)
146
+ - **Common Spacing**: 1rem, 1.5rem, 2rem, 3rem
147
+ - **Component Padding**: 1rem to 2rem
148
+ - **Grid Gaps**: 1rem to 1.5rem
149
+
150
+ ### Border Radius
151
+ - **Small**: 0.375rem (6px)
152
+ - **Medium**: 0.5rem (8px)
153
+ - **Large**: 0.75rem (12px)
154
+ - **Extra Large**: 1rem (16px)
155
+
156
+ ## 📱 Responsive Design
157
+
158
+ ### Breakpoints
159
+ - **Mobile**: < 768px
160
+ - **Tablet**: 768px - 1024px
161
+ - **Desktop**: > 1024px
162
+
163
+ ### Mobile Optimizations
164
+ - **Single column layouts** on mobile
165
+ - **Touch-friendly buttons** (44px minimum)
166
+ - **Optimized spacing** for small screens
167
+ - **Simplified navigation** for mobile users
168
+
169
+ ## ♿ Accessibility Features
170
+
171
+ ### Keyboard Navigation
172
+ - **Tab order** properly managed
173
+ - **Focus indicators** clearly visible
174
+ - **Keyboard shortcuts** for common actions
175
+ - **Skip links** for screen readers
176
+
177
+ ### Screen Reader Support
178
+ - **Semantic HTML** structure
179
+ - **ARIA labels** for complex components
180
+ - **Alt text** for all images
181
+ - **Descriptive link text**
182
+
183
+ ### Visual Accessibility
184
+ - **High contrast** color combinations
185
+ - **Large touch targets** (44px minimum)
186
+ - **Clear visual hierarchy** with proper headings
187
+ - **Consistent focus states**
188
+
189
+ ## 🔒 Security & Privacy
190
+
191
+ ### Enhanced Logging
192
+ - **Comprehensive event logging** for audit trails
193
+ - **User action tracking** for accountability
194
+ - **Error logging** for debugging
195
+ - **Performance monitoring** for optimization
196
+
197
+ ### Data Protection
198
+ - **Secure authentication** with OTP
199
+ - **Session management** with automatic timeouts
200
+ - **Input validation** to prevent injection attacks
201
+ - **Error handling** that doesn't expose sensitive data
202
+
203
+ ## 🚀 Performance Optimizations
204
+
205
+ ### Loading Performance
206
+ - **Optimized CSS** with minimal overhead
207
+ - **Efficient image processing** for AI recognition
208
+ - **Lazy loading** for non-critical components
209
+ - **Caching strategies** for repeated operations
210
+
211
+ ### User Experience
212
+ - **Smooth animations** (60fps target)
213
+ - **Instant feedback** for user actions
214
+ - **Progressive loading** for better perceived performance
215
+ - **Error recovery** with graceful fallbacks
216
+
217
+ ## 📊 Analytics & Monitoring
218
+
219
+ ### User Analytics
220
+ - **Visit tracking** with detailed metrics
221
+ - **Item processing** statistics
222
+ - **User engagement** measurements
223
+ - **Performance metrics** monitoring
224
+
225
+ ### System Monitoring
226
+ - **Error tracking** with detailed logs
227
+ - **Performance monitoring** for optimization
228
+ - **Usage analytics** for feature improvement
229
+ - **Health checks** for system reliability
230
+
231
+ ## 🔄 Maintenance & Updates
232
+
233
+ ### Version Control
234
+ - **Automatic backups** before any changes
235
+ - **Rollback capability** to previous versions
236
+ - **Change logging** for audit trails
237
+ - **Testing framework** for safe updates
238
+
239
+ ### Monitoring
240
+ - **Health checks** for system status
241
+ - **Performance monitoring** for optimization
242
+ - **Error tracking** for quick resolution
243
+ - **User feedback** collection for improvements
244
+
245
+ ## 🎯 Future Enhancements
246
+
247
+ ### Planned Features
248
+ - **Dark/light theme toggle**
249
+ - **Advanced analytics dashboard**
250
+ - **Mobile app version**
251
+ - **Offline capability**
252
+ - **Multi-language support**
253
+
254
+ ### Technical Improvements
255
+ - **Microservices architecture**
256
+ - **Real-time updates** with WebSockets
257
+ - **Advanced caching** strategies
258
+ - **Performance optimization** for large datasets
259
+
260
+ ## 📞 Support & Documentation
261
+
262
+ ### Getting Help
263
+ - **Comprehensive logging** for debugging
264
+ - **Error messages** with helpful suggestions
265
+ - **Documentation** for all features
266
+ - **Testing framework** for validation
267
+
268
+ ### Contributing
269
+ - **Clear code structure** for easy maintenance
270
+ - **Comprehensive testing** for reliability
271
+ - **Documentation** for all changes
272
+ - **Version control** for safe updates
273
+
274
+ ---
275
+
276
+ ## 🎉 Conclusion
277
+
278
+ The Care Count application now features a modern, industry-standard UI/UX that provides:
279
+
280
+ - **Professional appearance** that builds trust
281
+ - **Intuitive user experience** that reduces training time
282
+ - **Responsive design** that works on all devices
283
+ - **Accessibility features** that include all users
284
+ - **Robust testing** that ensures reliability
285
+ - **Safe deployment** with rollback capabilities
286
+
287
+ The improvements maintain all existing functionality while significantly enhancing the user experience and making the application more maintainable and scalable for future development.
__pycache__/streamlit_app.cpython-310.pyc ADDED
Binary file (27.1 kB). View file
 
__pycache__/streamlit_app_modern.cpython-310.pyc ADDED
Binary file (27.1 kB). View file
 
__pycache__/ui_improvements.cpython-310.pyc ADDED
Binary file (20.7 kB). View file
 
backups/deployment_log_20250923_202620.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "timestamp": "2025-09-23T20:26:01.318778",
4
+ "step": "testing",
5
+ "status": "STARTED",
6
+ "details": "Running pre-deployment tests"
7
+ },
8
+ {
9
+ "timestamp": "2025-09-23T20:26:12.234228",
10
+ "step": "import_test",
11
+ "status": "SUCCESS",
12
+ "details": "Modern app imports successfully"
13
+ },
14
+ {
15
+ "timestamp": "2025-09-23T20:26:12.235170",
16
+ "step": "ui_import_test",
17
+ "status": "SUCCESS",
18
+ "details": "UI improvements import successfully"
19
+ },
20
+ {
21
+ "timestamp": "2025-09-23T20:26:13.554936",
22
+ "step": "app_tests",
23
+ "status": "SUCCESS",
24
+ "details": "All app tests passed"
25
+ },
26
+ {
27
+ "timestamp": "2025-09-23T20:26:13.555445",
28
+ "step": "testing",
29
+ "status": "SUCCESS",
30
+ "details": "All tests passed"
31
+ },
32
+ {
33
+ "timestamp": "2025-09-23T20:26:13.555876",
34
+ "step": "deployment",
35
+ "status": "STARTED",
36
+ "details": "Deploying modern UI"
37
+ },
38
+ {
39
+ "timestamp": "2025-09-23T20:26:15.675932",
40
+ "step": "stop_app",
41
+ "status": "SUCCESS",
42
+ "details": "Stopped current app"
43
+ },
44
+ {
45
+ "timestamp": "2025-09-23T20:26:15.826651",
46
+ "step": "backup_creation",
47
+ "status": "SUCCESS",
48
+ "details": "Created backup: pre_modern_deployment_20250923_202615"
49
+ },
50
+ {
51
+ "timestamp": "2025-09-23T20:26:15.830996",
52
+ "step": "file_replacement",
53
+ "status": "SUCCESS",
54
+ "details": "Replaced app with modern version"
55
+ },
56
+ {
57
+ "timestamp": "2025-09-23T20:26:20.853515",
58
+ "step": "app_start",
59
+ "status": "SUCCESS",
60
+ "details": "Modern app started successfully"
61
+ }
62
+ ]
backups/deployment_log_20250923_203633.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "timestamp": "2025-09-23T20:36:21.190830",
4
+ "step": "testing",
5
+ "status": "STARTED",
6
+ "details": "Running pre-deployment tests"
7
+ },
8
+ {
9
+ "timestamp": "2025-09-23T20:36:25.672280",
10
+ "step": "import_test",
11
+ "status": "SUCCESS",
12
+ "details": "Modern app imports successfully"
13
+ },
14
+ {
15
+ "timestamp": "2025-09-23T20:36:25.672495",
16
+ "step": "ui_import_test",
17
+ "status": "SUCCESS",
18
+ "details": "UI improvements import successfully"
19
+ },
20
+ {
21
+ "timestamp": "2025-09-23T20:36:26.198747",
22
+ "step": "app_tests",
23
+ "status": "SUCCESS",
24
+ "details": "All app tests passed"
25
+ },
26
+ {
27
+ "timestamp": "2025-09-23T20:36:26.199337",
28
+ "step": "testing",
29
+ "status": "SUCCESS",
30
+ "details": "All tests passed"
31
+ },
32
+ {
33
+ "timestamp": "2025-09-23T20:36:26.199731",
34
+ "step": "deployment",
35
+ "status": "STARTED",
36
+ "details": "Deploying modern UI"
37
+ },
38
+ {
39
+ "timestamp": "2025-09-23T20:36:28.278741",
40
+ "step": "stop_app",
41
+ "status": "SUCCESS",
42
+ "details": "Stopped current app"
43
+ },
44
+ {
45
+ "timestamp": "2025-09-23T20:36:28.394861",
46
+ "step": "backup_creation",
47
+ "status": "SUCCESS",
48
+ "details": "Created backup: pre_modern_deployment_20250923_203628"
49
+ },
50
+ {
51
+ "timestamp": "2025-09-23T20:36:28.398794",
52
+ "step": "file_replacement",
53
+ "status": "SUCCESS",
54
+ "details": "Replaced app with modern version"
55
+ },
56
+ {
57
+ "timestamp": "2025-09-23T20:36:33.420662",
58
+ "step": "app_start",
59
+ "status": "SUCCESS",
60
+ "details": "Modern app started successfully"
61
+ }
62
+ ]
backups/deployment_log_20250923_204101.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
backups/deployment_log_20250923_204113.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "timestamp": "2025-09-23T20:41:07.876272",
4
+ "step": "rollback",
5
+ "status": "STARTED",
6
+ "details": "Rolling back to pre_modern_deployment_20250923_203628"
7
+ },
8
+ {
9
+ "timestamp": "2025-09-23T20:41:13.010207",
10
+ "step": "rollback",
11
+ "status": "SUCCESS",
12
+ "details": "Rolled back to pre_modern_deployment_20250923_203628"
13
+ }
14
+ ]
backups/deployment_log_20250923_205215.json ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "timestamp": "2025-09-23T20:52:02.354828",
4
+ "step": "testing",
5
+ "status": "STARTED",
6
+ "details": "Running pre-deployment tests"
7
+ },
8
+ {
9
+ "timestamp": "2025-09-23T20:52:07.829354",
10
+ "step": "import_test",
11
+ "status": "SUCCESS",
12
+ "details": "Modern app imports successfully"
13
+ },
14
+ {
15
+ "timestamp": "2025-09-23T20:52:07.829616",
16
+ "step": "ui_import_test",
17
+ "status": "SUCCESS",
18
+ "details": "UI improvements import successfully"
19
+ },
20
+ {
21
+ "timestamp": "2025-09-23T20:52:08.502096",
22
+ "step": "app_tests",
23
+ "status": "SUCCESS",
24
+ "details": "All app tests passed"
25
+ },
26
+ {
27
+ "timestamp": "2025-09-23T20:52:08.502609",
28
+ "step": "testing",
29
+ "status": "SUCCESS",
30
+ "details": "All tests passed"
31
+ },
32
+ {
33
+ "timestamp": "2025-09-23T20:52:08.503427",
34
+ "step": "deployment",
35
+ "status": "STARTED",
36
+ "details": "Deploying modern UI"
37
+ },
38
+ {
39
+ "timestamp": "2025-09-23T20:52:10.618570",
40
+ "step": "stop_app",
41
+ "status": "SUCCESS",
42
+ "details": "Stopped current app"
43
+ },
44
+ {
45
+ "timestamp": "2025-09-23T20:52:10.729497",
46
+ "step": "backup_creation",
47
+ "status": "SUCCESS",
48
+ "details": "Created backup: pre_modern_deployment_20250923_205210"
49
+ },
50
+ {
51
+ "timestamp": "2025-09-23T20:52:10.733425",
52
+ "step": "file_replacement",
53
+ "status": "SUCCESS",
54
+ "details": "Replaced app with modern version"
55
+ },
56
+ {
57
+ "timestamp": "2025-09-23T20:52:15.751869",
58
+ "step": "app_start",
59
+ "status": "SUCCESS",
60
+ "details": "Modern app started successfully"
61
+ }
62
+ ]
backups/deployment_log_20250924_092715.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
backups/hero_section_html_fix_20250923_203834/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/hero_section_html_fix_20250923_203834/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/hero_section_html_fix_20250923_203834/streamlit_app.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ), unsafe_allow_html=True)
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ))
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/indentation_fix_20250923_203937/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/indentation_fix_20250923_203937/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/indentation_fix_20250923_203937/streamlit_app.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ), unsafe_allow_html=True)
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ))
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/pre_modern_deployment_20250923_202615/backup_metadata.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backup_name": "pre_modern_deployment_20250923_202615",
3
+ "timestamp": "20250923_202615",
4
+ "description": "pre_modern_deployment",
5
+ "files_backed_up": [
6
+ "streamlit_app.py",
7
+ "streamlit_app_modern.py",
8
+ ".streamlit/secrets.toml",
9
+ "requirements.txt",
10
+ "ui_improvements.py",
11
+ "test_app.py"
12
+ ],
13
+ "git_status": {
14
+ "modified_files": [
15
+ "M requirements.txt",
16
+ " M streamlit_app.py",
17
+ "?? .streamlit/",
18
+ "?? .venv/",
19
+ "?? __pycache__/",
20
+ "?? backups/",
21
+ "?? care_count.log",
22
+ "?? deploy_modern.py",
23
+ "?? deployment.log",
24
+ "?? streamlit_app_backup_20250923_195853.py",
25
+ "?? streamlit_app_modern.py",
26
+ "?? test_app.py",
27
+ "?? test_results.log",
28
+ "?? ui_improvements.py"
29
+ ],
30
+ "return_code": 0
31
+ }
32
+ }
backups/pre_modern_deployment_20250923_202615/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/pre_modern_deployment_20250923_202615/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/pre_modern_deployment_20250923_202615/streamlit_app.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Delightful Laurier-themed UX + enterprise data hygiene
3
+ # - OTP email login (Supabase)
4
+ # - Shift tracking & idle/8pm auto sign-out
5
+ # - Human-friendly visit_code (with DB trigger or safe fallback)
6
+ # - VLM-assisted item name
7
+ # - RPC ingest with validation/quarantine (fallback to direct insert)
8
+ # - Volunteer Impact card (today + lifetime)
9
+
10
+ from __future__ import annotations
11
+
12
+ import os, io, time, base64, re, uuid, json
13
+ from datetime import datetime, timedelta
14
+ from typing import Optional
15
+
16
+ import pytz
17
+ import requests
18
+ import pandas as pd
19
+ import streamlit as st
20
+ from PIL import Image, ImageOps, ImageEnhance
21
+ from supabase import create_client, Client
22
+
23
+ # ------------------------ App config ------------------------
24
+ os.environ.setdefault("HOME", "/tmp")
25
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
26
+
27
+ TZ = os.getenv("APP_TZ", "America/Toronto")
28
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
29
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
30
+
31
+ def local_now() -> datetime:
32
+ return datetime.now(pytz.timezone(TZ))
33
+
34
+ st.set_page_config(page_title="Care Count", layout="centered")
35
+ st.markdown("""
36
+ <style>
37
+ /* Laurier theme vibes */
38
+ :root { --cc-purple:#6d28d9; --cc-gold:#fde047; --cc-bg:#0b1420; --cc-panel:#0f1a2a; --cc-border:#1d2a44; }
39
+ .block-container { padding-top: 2rem; }
40
+ h1, h2, .stMarkdown h1, .stMarkdown h2 { letter-spacing:.2px }
41
+ .cc-pill { display:inline-block; padding:4px 10px; border-radius:999px; background:var(--cc-gold); color:#111827; font-weight:700; font-size:12px; }
42
+ .cc-hint { background:#10233b; border:1px solid #1f3b5b; color:#e6e8f0; padding:12px 16px; border-radius:10px; }
43
+ .cc-hero { background:#0f1a2a; border:1px solid #1f2a44; padding:16px 18px; border-radius:14px; }
44
+ .cc-btn-primary button { background:var(--cc-purple)!important; color:#fff!important; border:0!important; }
45
+ .cc-danger button { background:#7f1d1d!important; color:#fff!important; }
46
+ .status-card { display:flex; gap:16px; flex-wrap:wrap; }
47
+ .card { background:#0f1a2a; border:1px solid #1f2a44; border-radius:14px; padding:14px 16px; min-width:200px; }
48
+ .card h4 { margin:0 0 6px 0; font-size:0.95rem; color:#cbd5e1 }
49
+ .card .big { font-size:1.6rem; font-weight:700; color:#e5e7eb }
50
+ .small { color:#9aa3b2; font-size:12px }
51
+ </style>
52
+ """, unsafe_allow_html=True)
53
+
54
+ st.title("💜💛 Care Count")
55
+ st.caption("Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time.")
56
+
57
+ # ------------------------ Secrets & client ------------------------
58
+ # Robust env/secrets helpers that work with Streamlit (st.secrets) and .env/CI.
59
+ # Order of precedence: OS env > Streamlit secrets > default.
60
+
61
+ # Optional: allow plain `python` runs to pick up a local .env if present
62
+ try:
63
+ from dotenv import load_dotenv # pip install python-dotenv (optional)
64
+ load_dotenv()
65
+ except Exception:
66
+ pass
67
+
68
+ # Make Streamlit secrets safe to access even when not launched via `streamlit run`
69
+ try:
70
+ _SECRETS = getattr(st, "secrets", {})
71
+ except Exception:
72
+ _SECRETS = {}
73
+
74
+ def _is_useful(val: Optional[str]) -> bool:
75
+ return val is not None and str(val).strip() != ""
76
+
77
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
78
+ """
79
+ Flat TOML usage:
80
+ SUPABASE_URL = "..."
81
+ SUPABASE_KEY = "..."
82
+ Looks in OS env first, then st.secrets, else returns default.
83
+ """
84
+ # 1) OS env
85
+ v = os.getenv(name)
86
+ if _is_useful(v):
87
+ return v
88
+
89
+ # 2) Streamlit secrets (flat)
90
+ try:
91
+ if hasattr(_SECRETS, "get"):
92
+ v = _SECRETS.get(name, default)
93
+ if _is_useful(v):
94
+ return v
95
+ except Exception:
96
+ pass
97
+
98
+ # 3) default
99
+ return default
100
+
101
+ def require_secret(name: str) -> str:
102
+ """
103
+ Same as get_secret but fails fast with a clear error if missing/blank.
104
+ """
105
+ v = get_secret(name, None)
106
+ if not _is_useful(v):
107
+ st.error(
108
+ f"Missing secret: {name}. "
109
+ "Ensure it exists as an environment variable or in .streamlit/secrets.toml"
110
+ )
111
+ st.stop()
112
+ return str(v)
113
+
114
+ # (Optional) lightweight visibility check during setup. Comment out in prod.
115
+ # st.write("Secrets present:",
116
+ # bool(get_secret("SUPABASE_URL")),
117
+ # bool(get_secret("SUPABASE_KEY")))
118
+
119
+ # Required Supabase credentials (flat keys)
120
+ SUPABASE_URL = require_secret("SUPABASE_URL")
121
+ SUPABASE_KEY = require_secret("SUPABASE_KEY") # anon key (RLS should be ON)
122
+
123
+ # Initialize Supabase client
124
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
125
+
126
+ # Optional provider/config (all flat keys with safe defaults)
127
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
128
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
129
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
130
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
131
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
132
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
133
+
134
+
135
+
136
+ st.caption(f"Provider: `{PROVIDER}` · Model: `{GEMMA_MODEL}` · TZ: `{TZ}`")
137
+
138
+ # ------------------------ Light events/audit (non-blocking) ------------------------
139
+ def log_event(action: str, actor: Optional[str], details: dict):
140
+ try:
141
+ sb.table("events").insert({"actor_email": actor, "action": action, "details": details}).execute()
142
+ except Exception:
143
+ pass
144
+
145
+ # ------------------------ Auth (Email OTP) ------------------------
146
+ def auth_block() -> tuple[bool, Optional[str]]:
147
+ if "auth_email" not in st.session_state:
148
+ st.session_state["auth_email"] = None
149
+ if "user_email" in st.session_state:
150
+ return True, st.session_state["user_email"]
151
+
152
+ st.subheader("Sign in")
153
+ with st.form("otp_request", clear_on_submit=False, border=False):
154
+ email = st.text_input("Email", value=st.session_state.get("auth_email") or "", autocomplete="email")
155
+ send = st.form_submit_button("Send login code")
156
+ if send:
157
+ if not email or "@" not in email:
158
+ st.error("Please enter a valid email.")
159
+ else:
160
+ try:
161
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
162
+ st.session_state["auth_email"] = email
163
+ st.success("We emailed you a one-time code. Enter it to continue.")
164
+ except Exception as e:
165
+ st.error(f"Could not send code: {e}")
166
+
167
+ if st.session_state.get("auth_email"):
168
+ with st.form("otp_verify", clear_on_submit=True, border=False):
169
+ code = st.text_input("Enter 6-digit code", max_chars=6)
170
+ ok = st.form_submit_button("Verify & start shift")
171
+ if ok:
172
+ try:
173
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
174
+ if res and res.user:
175
+ email = st.session_state["auth_email"]
176
+ # Idempotent upsert for volunteer & start shift
177
+ sb.table("volunteers").upsert({
178
+ "email": email,
179
+ "last_login_at": datetime.utcnow().isoformat(),
180
+ "shift_started_at": datetime.utcnow().isoformat(),
181
+ "shift_ended_at": None
182
+ }, on_conflict="email").execute()
183
+ st.session_state["user_email"] = email
184
+ st.session_state["shift_started"] = True
185
+ st.session_state["last_activity_at"] = local_now()
186
+ log_event("login", email, {"method":"otp"})
187
+ st.toast("Welcome back! Shift started. 💜", icon="✅")
188
+ return True, email
189
+ except Exception as e:
190
+ st.error(f"Verification failed: {e}")
191
+ return False, None
192
+
193
+ def end_shift(email: str, reason: str):
194
+ try:
195
+ sb.table("volunteers").update({"shift_ended_at": datetime.utcnow().isoformat()}).eq("email", email).execute()
196
+ log_event("shift_end", email, {"reason": reason})
197
+ except Exception:
198
+ pass
199
+
200
+ def guard_cutoff_and_idle(email: str):
201
+ now = local_now()
202
+ last = st.session_state.get("last_activity_at")
203
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
204
+ end_shift(email, "inactivity")
205
+ st.session_state.clear()
206
+ st.info("You were logged out due to inactivity. Thank you for volunteering today!")
207
+ st.stop()
208
+ st.session_state["last_activity_at"] = now
209
+
210
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
211
+ if now >= cutoff:
212
+ end_shift(email, "cutoff_8pm")
213
+ st.session_state.clear()
214
+ st.info("We close the day at 8pm. Your shift has been ended. Thank you so much!")
215
+ st.stop()
216
+
217
+ signed_in, user_email = auth_block()
218
+ if not signed_in:
219
+ st.stop()
220
+ guard_cutoff_and_idle(user_email)
221
+
222
+ # Welcome banner (visceral, kind)
223
+ st.markdown(
224
+ f"""<div class="cc-hero">
225
+ <div class="small">Welcome,</div>
226
+ <div style="font-weight:800;font-size:1.15rem"> {user_email}</div>
227
+ <div class="small">Thank you for showing up for the community today. 💜💛</div>
228
+ </div>""",
229
+ unsafe_allow_html=True
230
+ )
231
+
232
+ # Sign-out button (prominent)
233
+ c_signout = st.columns([1,1,6])[1]
234
+ with c_signout:
235
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
236
+ if st.button("🔒 Sign out"):
237
+ end_shift(user_email, "manual")
238
+ st.session_state.clear()
239
+ st.success("Signed out. See you next time!")
240
+ st.stop()
241
+ st.markdown('</div>', unsafe_allow_html=True)
242
+
243
+ # ------------------------ Volunteer Impact card ------------------------
244
+ def fetch_volunteer_row(email: str) -> dict | None:
245
+ try:
246
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
247
+ except Exception:
248
+ return None
249
+
250
+ def items_today(email: str) -> int:
251
+ try:
252
+ day = local_now().strftime("%Y-%m-%d")
253
+ # try partitioned table first
254
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
255
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
256
+ .eq("volunteer", email).execute().data
257
+ return len(data or [])
258
+ except Exception:
259
+ try:
260
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
261
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
262
+ .eq("volunteer", email).execute().data
263
+ return len(data or [])
264
+ except Exception:
265
+ return 0
266
+
267
+ vrow = fetch_volunteer_row(user_email) or {}
268
+ shift_started_at = vrow.get("shift_started_at")
269
+ lifetime_hours = vrow.get("total_hours") # may not exist yet; handled below
270
+ mins_active = 0
271
+ try:
272
+ if shift_started_at:
273
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
274
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
275
+ except Exception:
276
+ pass
277
+
278
+ st.markdown('<div class="status-card">', unsafe_allow_html=True)
279
+ st.markdown(f'<div class="card"><h4>Shift active</h4><div class="big">{mins_active} min</div><div class="small">since you signed in</div></div>', unsafe_allow_html=True)
280
+ st.markdown(f'<div class="card"><h4>Items you logged today</h4><div class="big">{items_today(user_email)}</div></div>', unsafe_allow_html=True)
281
+ if isinstance(lifetime_hours, (int, float)):
282
+ st.markdown(f'<div class="card"><h4>Lifetime hours</h4><div class="big">{round(float(lifetime_hours),1)}</div></div>', unsafe_allow_html=True)
283
+ st.markdown('</div>', unsafe_allow_html=True)
284
+
285
+ # ------------------------ Image helpers ------------------------
286
+ def _to_png_bytes(img: Image.Image) -> bytes:
287
+ b = io.BytesIO(); img.save(b, format="PNG"); return b.getvalue()
288
+
289
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
290
+ img = img.convert("RGB")
291
+ w, h = img.size
292
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
293
+ if scale < 1.0: img = img.resize((int(w * scale), int(h * scale)))
294
+ img = ImageOps.autocontrast(img, cutoff=2)
295
+ img = ImageEnhance.Brightness(img).enhance(1.06)
296
+ img = ImageEnhance.Contrast(img).enhance(1.05)
297
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
298
+ return img
299
+
300
+ # ------------------------ VLM client ------------------------
301
+ SYSTEM_HINT = "You label item being held in the image for a food bank. Return ONLY the item name."
302
+
303
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
304
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
305
+ url = f"{base_url.rstrip('/')}/chat/completions"
306
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
307
+ payload = {
308
+ "model": model_id,
309
+ "temperature": 0,
310
+ "messages": [
311
+ {"role": "system", "content": SYSTEM_HINT},
312
+ {"role": "user", "content": [
313
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
314
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
315
+ ]}
316
+ ]
317
+ }
318
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
319
+ if r.status_code != 200:
320
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
321
+ data = r.json()
322
+ return (data["choices"][0]["message"]["content"] or "").strip()
323
+
324
+ def gemma_item_name(img_bytes: bytes) -> str:
325
+ if PROVIDER == "nebius":
326
+ if not NEBIUS_API_KEY: raise RuntimeError("NEBIUS_API_KEY missing")
327
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
328
+ elif PROVIDER == "featherless":
329
+ if not FEATH_API_KEY: raise RuntimeError("FEATHERLESS_API_KEY missing")
330
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
331
+ else:
332
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
333
+
334
+ # ------------------------ Normalization ------------------------
335
+ BRANDS = {
336
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
337
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
338
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
339
+ }
340
+ GENERIC_TYPES = {
341
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
342
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
343
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
344
+ "crackers","cookies","soup","insect killer","spray"
345
+ }
346
+ def normalize_item_name(s: str) -> str:
347
+ s = (s or "").strip()
348
+ if not s: return ""
349
+ low = re.sub(r"[®™]", "", s.lower())
350
+ for b in BRANDS: low = low.replace(b, "")
351
+ chosen = None
352
+ for t in GENERIC_TYPES:
353
+ if t in low: chosen = t; break
354
+ cleaned = " ".join(low.split())
355
+ return (chosen or cleaned.title())[:120]
356
+
357
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
358
+ if not v: return None
359
+ v = re.sub(r"\s+", " ", v).strip()
360
+ return v[:maxlen] if v else None
361
+
362
+ # ------------------------ Visit flow ------------------------
363
+ st.subheader("🪪 Anonymous Student Visit")
364
+
365
+ active_visit = st.session_state.get("active_visit")
366
+
367
+ def fallback_visit_code() -> str:
368
+ """Readable visit_code if DB trigger isn't present."""
369
+ try:
370
+ day = local_now().strftime("%Y-%m-%d")
371
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
372
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
373
+ seq = len(todays) + 1
374
+ except Exception:
375
+ seq = int(time.time()) % 1000
376
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
377
+
378
+ c1, c2, c3 = st.columns(3)
379
+ with c1:
380
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
381
+ if not active_visit and st.button("Start Visit"):
382
+ try:
383
+ payload = {
384
+ "visit_code": fallback_visit_code(), # DB trigger will override if present
385
+ "started_at": datetime.utcnow().isoformat(),
386
+ "ended_at": None,
387
+ "created_by": user_email
388
+ }
389
+ v = sb.table("visits").insert(payload).execute().data[0]
390
+ # If trigger generated code, keep that
391
+ if not v.get("visit_code"):
392
+ v["visit_code"] = payload["visit_code"]
393
+ st.session_state["active_visit"] = v
394
+ st.success(f"Visit #{v['id']} started · code: **{v['visit_code']}**")
395
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
396
+ except Exception as e:
397
+ st.error(f"Could not start visit: {e}")
398
+ st.markdown('</div>', unsafe_allow_html=True)
399
+
400
+ with c2:
401
+ if active_visit and st.button("End Visit (Checkout)"):
402
+ try:
403
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
404
+ .eq("id", active_visit["id"]).execute()
405
+ st.success("Visit checked out. Ready for the next student.")
406
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
407
+ st.session_state.pop("active_visit", None)
408
+ except Exception as e:
409
+ st.error(f"Could not end visit: {e}")
410
+
411
+ with c3:
412
+ if st.session_state.get("active_visit"):
413
+ v = st.session_state["active_visit"]
414
+ st.caption(f"Active visit_id: {v['id']} · code: {v.get('visit_code','')}")
415
+ else:
416
+ st.caption("No active visit.")
417
+
418
+ # ------------------------ Identify item ------------------------
419
+ st.subheader("📸 Identify item from image")
420
+
421
+ c4, c5 = st.columns(2)
422
+ with c4: cam = st.camera_input("Use your phone or webcam")
423
+ with c5: up = st.file_uploader("…or upload an image", type=["png","jpg","jpeg"])
424
+
425
+ img_file = cam or up
426
+ if img_file:
427
+ img = Image.open(img_file).convert("RGB")
428
+ st.image(img, use_container_width=True)
429
+ if st.button("🔍 Ask model for item name"):
430
+ t0, raw = time.time(), ""
431
+ try:
432
+ pre = preprocess_for_label(img); raw = gemma_item_name(_to_png_bytes(pre))
433
+ except Exception as e:
434
+ st.error(f"Provider error: {e}"); log_event("vlm_error", user_email, {"error": str(e)})
435
+ norm = normalize_item_name(raw)
436
+ if raw: st.success(f"🧠 Model: **{raw}**")
437
+ st.info(f"✨ Normalized: **{norm or '(unknown)'}** · ⏱️ {time.time()-t0:.2f}s")
438
+ st.session_state["scanned_item_name"] = norm or raw or ""
439
+ st.session_state["last_activity_at"] = local_now()
440
+
441
+ # ------------------------ Log item ------------------------
442
+ st.subheader("📬 Log item to current visit")
443
+
444
+ item_name = st.text_input("Item name", value=st.session_state.get("scanned_item_name",""))
445
+ quantity = st.number_input("Quantity", min_value=1, max_value=9999, step=1, value=1)
446
+ category = st.text_input("Category (optional)")
447
+ unit = st.text_input("Unit (optional, e.g., 500 mL, 1 L, 250 g)")
448
+ barcode = st.text_input("Barcode (optional)")
449
+
450
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
451
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
452
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
453
+
454
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
455
+ category: Optional[str], unit: Optional[str],
456
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
457
+ """
458
+ Call the RPC safe_ingest_visit_item if it exists.
459
+ Returns (ok, msg). If function is missing, raises to trigger fallback.
460
+ """
461
+ try:
462
+ res = sb.rpc("safe_ingest_visit_item", {
463
+ "p_email": email,
464
+ "p_visit_id": v_id,
465
+ "p_item_name": name,
466
+ "p_qty": qty,
467
+ "p_category": category,
468
+ "p_unit": unit,
469
+ "p_barcode": barcode,
470
+ "p_ts": ts_iso,
471
+ "p_ingest_id": ingest_id
472
+ }).execute()
473
+ # RPC returns a setof (ok boolean, msg text) — normalize
474
+ rows = res.data if isinstance(res.data, list) else []
475
+ if rows:
476
+ r0 = rows[0]
477
+ ok = bool(r0.get("ok", False))
478
+ msg = str(r0.get("msg", ""))
479
+ return ok, msg or ("ok" if ok else "failed")
480
+ # Some PostgREST setups return {} — treat as ok
481
+ return True, "ok"
482
+ except Exception as e:
483
+ # If function missing (42883) or not exposed, re-raise to use fallback
484
+ raise e
485
+
486
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
487
+ category: Optional[str], unit: Optional[str],
488
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
489
+ """Write into visit_items_p if present, else visit_items (keeps app working)."""
490
+ payload = {
491
+ "visit_id": v_id,
492
+ "timestamp": ts_iso,
493
+ "volunteer": email,
494
+ "item_name": name,
495
+ "category": category,
496
+ "unit": unit,
497
+ "qty": qty,
498
+ "barcode": barcode,
499
+ "weather_type": None,
500
+ "temp_c": None,
501
+ "ingest_id": ingest_id
502
+ }
503
+ try:
504
+ sb.table("visit_items_p").insert(payload).execute()
505
+ except Exception:
506
+ # legacy table fallback
507
+ payload.pop("ingest_id", None)
508
+ sb.table("visit_items").insert(payload).execute()
509
+
510
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
511
+ save_disabled = not st.session_state.get("active_visit")
512
+ if st.button("✅ Save Item to Visit", disabled=save_disabled):
513
+ v = st.session_state.get("active_visit")
514
+ if not v:
515
+ st.warning("Start a visit first, then save items.")
516
+ else:
517
+ name_clean = clean_text(item_name, 120)
518
+ if not name_clean:
519
+ st.warning("Item name is required.")
520
+ else:
521
+ ts_iso = datetime.utcnow().isoformat()
522
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
523
+ try:
524
+ ok, msg = try_rpc_ingest(
525
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
526
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
527
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
528
+ )
529
+ if ok:
530
+ st.success("Item logged ✅")
531
+ else:
532
+ st.warning(f"Ingest said: {msg}. (Will try direct insert.)")
533
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
534
+ clean_text(category,80), clean_text(unit,40),
535
+ clean_text(barcode,64), ts_iso, ingest_id)
536
+ st.success("Item logged ✅ (fallback)")
537
+ except Exception as e:
538
+ # RPC missing or failed → direct insert
539
+ try:
540
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
541
+ clean_text(category,80), clean_text(unit,40),
542
+ clean_text(barcode,64), ts_iso, ingest_id)
543
+ st.success("Item logged ✅ (fallback)")
544
+ except Exception as e2:
545
+ st.error(f"Ingest failed: {e2}")
546
+ st.session_state["last_activity_at"] = local_now()
547
+ st.session_state["scanned_item_name"] = name_clean
548
+ st.markdown('</div>', unsafe_allow_html=True)
549
+
550
+ # ------------------------ Visit items view + delete ------------------------
551
+ def load_items_for_visit(visit_id: int) -> list[dict]:
552
+ # Prefer partitioned table
553
+ try:
554
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
555
+ .order("timestamp", desc=True).limit(500).execute().data or []
556
+ except Exception:
557
+ try:
558
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
559
+ .order("timestamp", desc=True).limit(500).execute().data or []
560
+ except Exception:
561
+ return []
562
+
563
+ def delete_item(table: str, item_id: int):
564
+ try:
565
+ sb.table(table).delete().eq("id", item_id).execute()
566
+ except Exception as e:
567
+ raise e
568
+
569
+ if st.session_state.get("active_visit"):
570
+ st.subheader("🧾 Items in this visit")
571
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
572
+ if rows:
573
+ df = pd.DataFrame(rows)
574
+ st.dataframe(df, use_container_width=True)
575
+ with st.expander("🗑️ Delete an item (if mis-logged)"):
576
+ ids = [r["id"] for r in rows if "id" in r]
577
+ choice = st.selectbox("Choose item id", ids) if ids else None
578
+ st.markdown('<div class="cc-danger">', unsafe_allow_html=True)
579
+ if st.button("Delete selected", disabled=not bool(ids)):
580
+ if choice is None:
581
+ st.warning("Pick an id.")
582
+ else:
583
+ # try both tables safely
584
+ try:
585
+ delete_item("visit_items_p", int(choice))
586
+ except Exception:
587
+ delete_item("visit_items", int(choice))
588
+ st.success("Deleted.")
589
+ st.markdown('</div>', unsafe_allow_html=True)
590
+ else:
591
+ st.caption("No items logged for this visit yet.")
592
+
593
+ # ------------------------ Analytics (light) ------------------------
594
+ st.subheader("📈 Today")
595
+ try:
596
+ daily = sb.table("v_daily_activity").select("*").execute().data
597
+ today_str = local_now().strftime("%Y-%m-%d")
598
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
599
+ visits = int(today["visits"]) if today and "visits" in today else 0
600
+ items = int(today["items"]) if today and "items" in today else 0
601
+ st.markdown(f"**Visits:** {visits} · **Items:** {items}")
602
+ except Exception:
603
+ st.caption("Analytics view unavailable yet.")
604
+
605
+ # ------------------------ Gentle reminder ------------------------
606
+ st.markdown(
607
+ """<div class="cc-hint">
608
+ 💡 When you’re done, please <b>End Visit</b> and <b>Sign out</b>.<br>
609
+ We’ll auto-sign you out after inactivity or at 8pm local time. 💜
610
+ </div>""",
611
+ unsafe_allow_html=True
612
+ )
backups/pre_modern_deployment_20250923_202615/streamlit_app_modern.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ))
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ))
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/pre_modern_deployment_20250923_202615/test_app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Care Count App Testing Framework
4
+ Tests UI/UX changes and backend functionality
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import time
10
+ import logging
11
+ from datetime import datetime
12
+ from typing import Dict, Any, List
13
+ import subprocess
14
+ import requests
15
+ from pathlib import Path
16
+
17
+ # Setup logging
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(levelname)s - %(message)s',
21
+ handlers=[
22
+ logging.FileHandler('test_results.log'),
23
+ logging.StreamHandler()
24
+ ]
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ class CareCountTester:
29
+ def __init__(self):
30
+ self.test_results = []
31
+ self.app_url = "http://localhost:8501"
32
+ self.backup_dir = Path("backups")
33
+ self.backup_dir.mkdir(exist_ok=True)
34
+
35
+ def create_backup(self, description: str = "manual_backup"):
36
+ """Create a timestamped backup of current app state"""
37
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
38
+ backup_name = f"{description}_{timestamp}"
39
+
40
+ # Backup main files
41
+ files_to_backup = [
42
+ "streamlit_app.py",
43
+ ".streamlit/secrets.toml",
44
+ "requirements.txt"
45
+ ]
46
+
47
+ backup_path = self.backup_dir / backup_name
48
+ backup_path.mkdir(exist_ok=True)
49
+
50
+ for file_path in files_to_backup:
51
+ if os.path.exists(file_path):
52
+ subprocess.run(["cp", file_path, str(backup_path / os.path.basename(file_path))])
53
+
54
+ logger.info(f"Backup created: {backup_name}")
55
+ return backup_name
56
+
57
+ def restore_backup(self, backup_name: str):
58
+ """Restore from a backup"""
59
+ backup_path = self.backup_dir / backup_name
60
+
61
+ if not backup_path.exists():
62
+ logger.error(f"Backup {backup_name} not found")
63
+ return False
64
+
65
+ # Restore files
66
+ files_to_restore = [
67
+ "streamlit_app.py",
68
+ ".streamlit/secrets.toml",
69
+ "requirements.txt"
70
+ ]
71
+
72
+ for file_path in files_to_restore:
73
+ backup_file = backup_path / os.path.basename(file_path)
74
+ if backup_file.exists():
75
+ subprocess.run(["cp", str(backup_file), file_path])
76
+
77
+ logger.info(f"Restored from backup: {backup_name}")
78
+ return True
79
+
80
+ def test_app_startup(self) -> bool:
81
+ """Test if the app starts without errors"""
82
+ try:
83
+ # Check if app is already running
84
+ response = requests.get(self.app_url, timeout=5)
85
+ if response.status_code == 200:
86
+ logger.info("✅ App is running and accessible")
87
+ return True
88
+ else:
89
+ logger.error(f"❌ App returned status code: {response.status_code}")
90
+ return False
91
+ except requests.exceptions.RequestException as e:
92
+ logger.error(f"❌ App startup test failed: {e}")
93
+ return False
94
+
95
+ def test_ui_elements(self) -> Dict[str, bool]:
96
+ """Test key UI elements are present"""
97
+ results = {}
98
+ try:
99
+ response = requests.get(self.app_url, timeout=10)
100
+ content = response.text.lower()
101
+
102
+ # Test for key UI elements
103
+ ui_tests = {
104
+ "title_present": "care count" in content,
105
+ "signin_form": "sign in" in content or "email" in content,
106
+ "camera_input": "camera" in content or "webcam" in content,
107
+ "file_upload": "upload" in content or "file" in content,
108
+ "visit_management": "visit" in content,
109
+ "item_logging": "item" in content,
110
+ "css_styling": "style" in content or "css" in content
111
+ }
112
+
113
+ for test_name, result in ui_tests.items():
114
+ results[test_name] = result
115
+ status = "✅" if result else "❌"
116
+ logger.info(f"{status} {test_name}: {result}")
117
+
118
+ return results
119
+
120
+ except Exception as e:
121
+ logger.error(f"❌ UI elements test failed: {e}")
122
+ return {}
123
+
124
+ def test_responsive_design(self) -> bool:
125
+ """Test if the app has responsive design elements"""
126
+ try:
127
+ response = requests.get(self.app_url, timeout=10)
128
+ content = response.text
129
+
130
+ # Check for responsive design indicators
131
+ responsive_indicators = [
132
+ "container" in content,
133
+ "column" in content,
134
+ "responsive" in content,
135
+ "mobile" in content
136
+ ]
137
+
138
+ responsive_score = sum(responsive_indicators) / len(responsive_indicators)
139
+ is_responsive = responsive_score >= 0.5
140
+
141
+ logger.info(f"📱 Responsive design score: {responsive_score:.2f} ({'✅' if is_responsive else '❌'})")
142
+ return is_responsive
143
+
144
+ except Exception as e:
145
+ logger.error(f"❌ Responsive design test failed: {e}")
146
+ return False
147
+
148
+ def test_performance(self) -> Dict[str, float]:
149
+ """Test app performance metrics"""
150
+ try:
151
+ start_time = time.time()
152
+ response = requests.get(self.app_url, timeout=30)
153
+ load_time = time.time() - start_time
154
+
155
+ results = {
156
+ "load_time": load_time,
157
+ "status_code": response.status_code,
158
+ "content_size": len(response.content)
159
+ }
160
+
161
+ logger.info(f"⚡ Load time: {load_time:.2f}s")
162
+ logger.info(f"📊 Content size: {len(response.content)} bytes")
163
+
164
+ return results
165
+
166
+ except Exception as e:
167
+ logger.error(f"❌ Performance test failed: {e}")
168
+ return {}
169
+
170
+ def run_all_tests(self) -> Dict[str, Any]:
171
+ """Run all tests and return comprehensive results"""
172
+ logger.info("🧪 Starting comprehensive app testing...")
173
+
174
+ # Create backup before testing
175
+ backup_name = self.create_backup("pre_test_backup")
176
+
177
+ test_results = {
178
+ "timestamp": datetime.now().isoformat(),
179
+ "backup_created": backup_name,
180
+ "startup_test": self.test_app_startup(),
181
+ "ui_elements": self.test_ui_elements(),
182
+ "responsive_design": self.test_responsive_design(),
183
+ "performance": self.test_performance()
184
+ }
185
+
186
+ # Calculate overall score
187
+ ui_score = sum(test_results["ui_elements"].values()) / len(test_results["ui_elements"]) if test_results["ui_elements"] else 0
188
+ overall_score = (
189
+ (1 if test_results["startup_test"] else 0) +
190
+ ui_score +
191
+ (1 if test_results["responsive_design"] else 0)
192
+ ) / 3
193
+
194
+ test_results["overall_score"] = overall_score
195
+ test_results["status"] = "PASS" if overall_score >= 0.8 else "FAIL"
196
+
197
+ logger.info(f"🎯 Overall test score: {overall_score:.2f} ({test_results['status']})")
198
+
199
+ return test_results
200
+
201
+ def main():
202
+ """Main testing function"""
203
+ tester = CareCountTester()
204
+
205
+ if len(sys.argv) > 1:
206
+ command = sys.argv[1]
207
+
208
+ if command == "backup":
209
+ description = sys.argv[2] if len(sys.argv) > 2 else "manual_backup"
210
+ tester.create_backup(description)
211
+ elif command == "restore":
212
+ if len(sys.argv) > 2:
213
+ tester.restore_backup(sys.argv[2])
214
+ else:
215
+ print("Usage: python test_app.py restore <backup_name>")
216
+ elif command == "test":
217
+ results = tester.run_all_tests()
218
+ print(f"\n📋 Test Results Summary:")
219
+ print(f"Status: {results['status']}")
220
+ print(f"Score: {results['overall_score']:.2f}")
221
+ print(f"Backup: {results['backup_created']}")
222
+ else:
223
+ print("Available commands: backup, restore, test")
224
+ else:
225
+ # Run all tests by default
226
+ results = tester.run_all_tests()
227
+ print(f"\n📋 Test Results Summary:")
228
+ print(f"Status: {results['status']}")
229
+ print(f"Score: {results['overall_score']:.2f}")
230
+ print(f"Backup: {results['backup_created']}")
231
+
232
+ if __name__ == "__main__":
233
+ main()
backups/pre_modern_deployment_20250923_202615/ui_improvements.py ADDED
@@ -0,0 +1,494 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Care Count UI/UX Improvements Module
3
+ Industry-standard UI components and styling
4
+ """
5
+
6
+ import streamlit as st
7
+ from typing import Dict, Any, Optional
8
+ import base64
9
+ from pathlib import Path
10
+
11
+ class ModernUIComponents:
12
+ """Modern UI components for Care Count app"""
13
+
14
+ @staticmethod
15
+ def get_modern_css() -> str:
16
+ """Return modern, industry-standard CSS"""
17
+ return """
18
+ <style>
19
+ /* Modern Design System */
20
+ :root {
21
+ /* Color Palette - Laurier Theme Enhanced */
22
+ --primary-purple: #6d28d9;
23
+ --primary-gold: #fde047;
24
+ --primary-dark: #0b1420;
25
+ --secondary-dark: #0f1a2a;
26
+ --accent-blue: #3b82f6;
27
+ --accent-green: #10b981;
28
+ --accent-red: #ef4444;
29
+ --accent-orange: #f59e0b;
30
+
31
+ /* Neutral Colors */
32
+ --gray-50: #f9fafb;
33
+ --gray-100: #f3f4f6;
34
+ --gray-200: #e5e7eb;
35
+ --gray-300: #d1d5db;
36
+ --gray-400: #9ca3af;
37
+ --gray-500: #6b7280;
38
+ --gray-600: #4b5563;
39
+ --gray-700: #374151;
40
+ --gray-800: #1f2937;
41
+ --gray-900: #111827;
42
+
43
+ /* Typography */
44
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ --font-size-xs: 0.75rem;
46
+ --font-size-sm: 0.875rem;
47
+ --font-size-base: 1rem;
48
+ --font-size-lg: 1.125rem;
49
+ --font-size-xl: 1.25rem;
50
+ --font-size-2xl: 1.5rem;
51
+ --font-size-3xl: 1.875rem;
52
+
53
+ /* Spacing */
54
+ --space-1: 0.25rem;
55
+ --space-2: 0.5rem;
56
+ --space-3: 0.75rem;
57
+ --space-4: 1rem;
58
+ --space-5: 1.25rem;
59
+ --space-6: 1.5rem;
60
+ --space-8: 2rem;
61
+ --space-10: 2.5rem;
62
+ --space-12: 3rem;
63
+
64
+ /* Border Radius */
65
+ --radius-sm: 0.375rem;
66
+ --radius-md: 0.5rem;
67
+ --radius-lg: 0.75rem;
68
+ --radius-xl: 1rem;
69
+ --radius-2xl: 1.5rem;
70
+
71
+ /* Shadows */
72
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
73
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
74
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
75
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
76
+ }
77
+
78
+ /* Global Styles */
79
+ * {
80
+ box-sizing: border-box;
81
+ }
82
+
83
+ body {
84
+ font-family: var(--font-family);
85
+ background: var(--primary-dark);
86
+ color: var(--gray-100);
87
+ line-height: 1.6;
88
+ }
89
+
90
+ /* Streamlit Overrides */
91
+ .main .block-container {
92
+ padding: var(--space-6) var(--space-4);
93
+ max-width: 1200px;
94
+ }
95
+
96
+ .stApp {
97
+ background: var(--primary-dark);
98
+ }
99
+
100
+ /* Typography */
101
+ h1, h2, h3, h4, h5, h6 {
102
+ font-weight: 700;
103
+ letter-spacing: -0.025em;
104
+ color: var(--gray-100);
105
+ }
106
+
107
+ h1 {
108
+ font-size: var(--font-size-3xl);
109
+ margin-bottom: var(--space-6);
110
+ }
111
+
112
+ h2 {
113
+ font-size: var(--font-size-2xl);
114
+ margin-bottom: var(--space-4);
115
+ }
116
+
117
+ h3 {
118
+ font-size: var(--font-size-xl);
119
+ margin-bottom: var(--space-3);
120
+ }
121
+
122
+ /* Modern Cards */
123
+ .modern-card {
124
+ background: var(--secondary-dark);
125
+ border: 1px solid var(--gray-700);
126
+ border-radius: var(--radius-xl);
127
+ padding: var(--space-6);
128
+ margin-bottom: var(--space-4);
129
+ box-shadow: var(--shadow-lg);
130
+ transition: all 0.2s ease;
131
+ }
132
+
133
+ .modern-card:hover {
134
+ border-color: var(--primary-purple);
135
+ box-shadow: var(--shadow-xl);
136
+ }
137
+
138
+ /* Status Cards */
139
+ .status-grid {
140
+ display: grid;
141
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
142
+ gap: var(--space-4);
143
+ margin-bottom: var(--space-6);
144
+ }
145
+
146
+ .status-card {
147
+ background: var(--secondary-dark);
148
+ border: 1px solid var(--gray-700);
149
+ border-radius: var(--radius-lg);
150
+ padding: var(--space-4);
151
+ text-align: center;
152
+ transition: all 0.2s ease;
153
+ }
154
+
155
+ .status-card:hover {
156
+ transform: translateY(-2px);
157
+ box-shadow: var(--shadow-lg);
158
+ }
159
+
160
+ .status-card h4 {
161
+ margin: 0 0 var(--space-2) 0;
162
+ font-size: var(--font-size-sm);
163
+ color: var(--gray-400);
164
+ text-transform: uppercase;
165
+ letter-spacing: 0.05em;
166
+ }
167
+
168
+ .status-card .value {
169
+ font-size: var(--font-size-2xl);
170
+ font-weight: 800;
171
+ color: var(--primary-gold);
172
+ margin: 0;
173
+ }
174
+
175
+ .status-card .subtitle {
176
+ font-size: var(--font-size-xs);
177
+ color: var(--gray-500);
178
+ margin: var(--space-1) 0 0 0;
179
+ }
180
+
181
+ /* Modern Buttons */
182
+ .modern-btn {
183
+ background: var(--primary-purple);
184
+ color: white;
185
+ border: none;
186
+ border-radius: var(--radius-md);
187
+ padding: var(--space-3) var(--space-6);
188
+ font-weight: 600;
189
+ font-size: var(--font-size-sm);
190
+ cursor: pointer;
191
+ transition: all 0.2s ease;
192
+ box-shadow: var(--shadow-sm);
193
+ }
194
+
195
+ .modern-btn:hover {
196
+ background: #5b21b6;
197
+ transform: translateY(-1px);
198
+ box-shadow: var(--shadow-md);
199
+ }
200
+
201
+ .modern-btn:active {
202
+ transform: translateY(0);
203
+ }
204
+
205
+ .modern-btn-secondary {
206
+ background: var(--gray-700);
207
+ color: var(--gray-100);
208
+ }
209
+
210
+ .modern-btn-secondary:hover {
211
+ background: var(--gray-600);
212
+ }
213
+
214
+ .modern-btn-success {
215
+ background: var(--accent-green);
216
+ }
217
+
218
+ .modern-btn-success:hover {
219
+ background: #059669;
220
+ }
221
+
222
+ .modern-btn-danger {
223
+ background: var(--accent-red);
224
+ }
225
+
226
+ .modern-btn-danger:hover {
227
+ background: #dc2626;
228
+ }
229
+
230
+ /* Form Elements */
231
+ .modern-input {
232
+ background: var(--secondary-dark);
233
+ border: 1px solid var(--gray-600);
234
+ border-radius: var(--radius-md);
235
+ padding: var(--space-3);
236
+ color: var(--gray-100);
237
+ font-size: var(--font-size-sm);
238
+ transition: all 0.2s ease;
239
+ }
240
+
241
+ .modern-input:focus {
242
+ outline: none;
243
+ border-color: var(--primary-purple);
244
+ box-shadow: 0 0 0 3px rgb(109 40 217 / 0.1);
245
+ }
246
+
247
+ /* Hero Section */
248
+ .hero-section {
249
+ background: linear-gradient(135deg, var(--primary-purple) 0%, var(--accent-blue) 100%);
250
+ border-radius: var(--radius-2xl);
251
+ padding: var(--space-8);
252
+ margin-bottom: var(--space-8);
253
+ text-align: center;
254
+ color: white;
255
+ }
256
+
257
+ .hero-section h1 {
258
+ color: white;
259
+ margin-bottom: var(--space-4);
260
+ }
261
+
262
+ .hero-section p {
263
+ font-size: var(--font-size-lg);
264
+ opacity: 0.9;
265
+ margin: 0;
266
+ }
267
+
268
+ /* Progress Indicators */
269
+ .progress-bar {
270
+ background: var(--gray-700);
271
+ border-radius: var(--radius-lg);
272
+ height: 8px;
273
+ overflow: hidden;
274
+ margin: var(--space-2) 0;
275
+ }
276
+
277
+ .progress-fill {
278
+ background: linear-gradient(90deg, var(--primary-purple), var(--primary-gold));
279
+ height: 100%;
280
+ transition: width 0.3s ease;
281
+ }
282
+
283
+ /* Badges */
284
+ .badge {
285
+ display: inline-block;
286
+ padding: var(--space-1) var(--space-2);
287
+ border-radius: var(--radius-sm);
288
+ font-size: var(--font-size-xs);
289
+ font-weight: 600;
290
+ text-transform: uppercase;
291
+ letter-spacing: 0.05em;
292
+ }
293
+
294
+ .badge-success {
295
+ background: var(--accent-green);
296
+ color: white;
297
+ }
298
+
299
+ .badge-warning {
300
+ background: var(--accent-orange);
301
+ color: white;
302
+ }
303
+
304
+ .badge-danger {
305
+ background: var(--accent-red);
306
+ color: white;
307
+ }
308
+
309
+ .badge-info {
310
+ background: var(--accent-blue);
311
+ color: white;
312
+ }
313
+
314
+ /* Responsive Design */
315
+ @media (max-width: 768px) {
316
+ .main .block-container {
317
+ padding: var(--space-4) var(--space-2);
318
+ }
319
+
320
+ .status-grid {
321
+ grid-template-columns: 1fr;
322
+ }
323
+
324
+ .hero-section {
325
+ padding: var(--space-6);
326
+ }
327
+
328
+ h1 {
329
+ font-size: var(--font-size-2xl);
330
+ }
331
+ }
332
+
333
+ /* Loading States */
334
+ .loading {
335
+ display: inline-block;
336
+ width: 20px;
337
+ height: 20px;
338
+ border: 3px solid var(--gray-600);
339
+ border-radius: 50%;
340
+ border-top-color: var(--primary-purple);
341
+ animation: spin 1s ease-in-out infinite;
342
+ }
343
+
344
+ @keyframes spin {
345
+ to { transform: rotate(360deg); }
346
+ }
347
+
348
+ /* Animations */
349
+ .fade-in {
350
+ animation: fadeIn 0.5s ease-in;
351
+ }
352
+
353
+ @keyframes fadeIn {
354
+ from { opacity: 0; transform: translateY(20px); }
355
+ to { opacity: 1; transform: translateY(0); }
356
+ }
357
+
358
+ .slide-in {
359
+ animation: slideIn 0.3s ease-out;
360
+ }
361
+
362
+ @keyframes slideIn {
363
+ from { transform: translateX(-100%); }
364
+ to { transform: translateX(0); }
365
+ }
366
+
367
+ /* Accessibility */
368
+ .sr-only {
369
+ position: absolute;
370
+ width: 1px;
371
+ height: 1px;
372
+ padding: 0;
373
+ margin: -1px;
374
+ overflow: hidden;
375
+ clip: rect(0, 0, 0, 0);
376
+ white-space: nowrap;
377
+ border: 0;
378
+ }
379
+
380
+ /* Focus states for accessibility */
381
+ button:focus,
382
+ input:focus,
383
+ select:focus,
384
+ textarea:focus {
385
+ outline: 2px solid var(--primary-purple);
386
+ outline-offset: 2px;
387
+ }
388
+ </style>
389
+ """
390
+
391
+ @staticmethod
392
+ def create_hero_section(title: str, subtitle: str, user_email: str = None) -> str:
393
+ """Create a modern hero section"""
394
+ if user_email:
395
+ return f"""
396
+ <div class="hero-section fade-in">
397
+ <h1>💜💛 {title}</h1>
398
+ <p>{subtitle}</p>
399
+ <div style="margin-top: var(--space-4); padding: var(--space-4); background: rgba(255,255,255,0.1); border-radius: var(--radius-lg);">
400
+ <div style="font-size: var(--font-size-sm); opacity: 0.8;">Welcome,</div>
401
+ <div style="font-weight: 800; font-size: var(--font-size-lg); margin: var(--space-1) 0;">{user_email}</div>
402
+ <div style="font-size: var(--font-size-sm); opacity: 0.8;">Thank you for showing up for the community today. 💜💛</div>
403
+ </div>
404
+ </div>
405
+ """
406
+ else:
407
+ return f"""
408
+ <div class="hero-section fade-in">
409
+ <h1>💜💛 {title}</h1>
410
+ <p>{subtitle}</p>
411
+ </div>
412
+ """
413
+
414
+ @staticmethod
415
+ def create_status_cards(data: Dict[str, Any]) -> str:
416
+ """Create modern status cards"""
417
+ cards_html = '<div class="status-grid">'
418
+
419
+ for key, value in data.items():
420
+ if key == "shift_active":
421
+ cards_html += f"""
422
+ <div class="status-card slide-in">
423
+ <h4>Shift Active</h4>
424
+ <div class="value">{value}</div>
425
+ <div class="subtitle">since you signed in</div>
426
+ </div>
427
+ """
428
+ elif key == "items_today":
429
+ cards_html += f"""
430
+ <div class="status-card slide-in">
431
+ <h4>Items Logged Today</h4>
432
+ <div class="value">{value}</div>
433
+ <div class="subtitle">items processed</div>
434
+ </div>
435
+ """
436
+ elif key == "lifetime_hours":
437
+ cards_html += f"""
438
+ <div class="status-card slide-in">
439
+ <h4>Lifetime Hours</h4>
440
+ <div class="value">{value}</div>
441
+ <div class="subtitle">volunteer hours</div>
442
+ </div>
443
+ """
444
+
445
+ cards_html += '</div>'
446
+ return cards_html
447
+
448
+ @staticmethod
449
+ def create_modern_form_section(title: str, description: str = None) -> str:
450
+ """Create a modern form section header"""
451
+ desc_html = f'<p style="color: var(--gray-400); margin-bottom: var(--space-4);">{description}</p>' if description else ''
452
+ return f"""
453
+ <div class="modern-card fade-in">
454
+ <h3>{title}</h3>
455
+ {desc_html}
456
+ """
457
+
458
+ @staticmethod
459
+ def create_progress_indicator(current: int, total: int, label: str) -> str:
460
+ """Create a modern progress indicator"""
461
+ percentage = (current / total * 100) if total > 0 else 0
462
+ return f"""
463
+ <div style="margin: var(--space-4) 0;">
464
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2);">
465
+ <span style="font-weight: 600; color: var(--gray-300);">{label}</span>
466
+ <span style="font-weight: 700; color: var(--primary-gold);">{current}/{total}</span>
467
+ </div>
468
+ <div class="progress-bar">
469
+ <div class="progress-fill" style="width: {percentage}%;"></div>
470
+ </div>
471
+ </div>
472
+ """
473
+
474
+ @staticmethod
475
+ def create_badge(text: str, variant: str = "info") -> str:
476
+ """Create a modern badge"""
477
+ return f'<span class="badge badge-{variant}">{text}</span>'
478
+
479
+ @staticmethod
480
+ def create_loading_spinner() -> str:
481
+ """Create a loading spinner"""
482
+ return '<div class="loading"></div>'
483
+
484
+ def apply_modern_ui():
485
+ """Apply modern UI styling to the Streamlit app"""
486
+ st.markdown(ModernUIComponents.get_modern_css(), unsafe_allow_html=True)
487
+
488
+ def create_modern_layout():
489
+ """Create a modern layout structure"""
490
+ return {
491
+ "container_style": "max-width: 1200px; margin: 0 auto;",
492
+ "sidebar_style": "background: var(--secondary-dark); border-right: 1px solid var(--gray-700);",
493
+ "main_style": "background: var(--primary-dark); padding: var(--space-6);"
494
+ }
backups/pre_modern_deployment_20250923_203628/backup_metadata.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backup_name": "pre_modern_deployment_20250923_203628",
3
+ "timestamp": "20250923_203628",
4
+ "description": "pre_modern_deployment",
5
+ "files_backed_up": [
6
+ "streamlit_app.py",
7
+ "streamlit_app_modern.py",
8
+ ".streamlit/secrets.toml",
9
+ "requirements.txt",
10
+ "ui_improvements.py",
11
+ "test_app.py"
12
+ ],
13
+ "git_status": {
14
+ "modified_files": [
15
+ "M requirements.txt",
16
+ " M streamlit_app.py",
17
+ "?? .streamlit/",
18
+ "?? .venv/",
19
+ "?? DEPLOYMENT_QUICK_REFERENCE.md",
20
+ "?? UI_IMPROVEMENTS_GUIDE.md",
21
+ "?? __pycache__/",
22
+ "?? backups/",
23
+ "?? care_count.log",
24
+ "?? deploy_modern.py",
25
+ "?? deployment.log",
26
+ "?? streamlit_app_backup_20250923_195853.py",
27
+ "?? streamlit_app_modern.py",
28
+ "?? test_app.py",
29
+ "?? test_results.log",
30
+ "?? ui_improvements.py"
31
+ ],
32
+ "return_code": 0
33
+ }
34
+ }
backups/pre_modern_deployment_20250923_203628/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/pre_modern_deployment_20250923_203628/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/pre_modern_deployment_20250923_203628/streamlit_app.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ), unsafe_allow_html=True)
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ), unsafe_allow_html=True)
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/pre_modern_deployment_20250923_203628/streamlit_app_modern.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ))
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ))
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/pre_modern_deployment_20250923_203628/test_app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Care Count App Testing Framework
4
+ Tests UI/UX changes and backend functionality
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import time
10
+ import logging
11
+ from datetime import datetime
12
+ from typing import Dict, Any, List
13
+ import subprocess
14
+ import requests
15
+ from pathlib import Path
16
+
17
+ # Setup logging
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(levelname)s - %(message)s',
21
+ handlers=[
22
+ logging.FileHandler('test_results.log'),
23
+ logging.StreamHandler()
24
+ ]
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ class CareCountTester:
29
+ def __init__(self):
30
+ self.test_results = []
31
+ self.app_url = "http://localhost:8501"
32
+ self.backup_dir = Path("backups")
33
+ self.backup_dir.mkdir(exist_ok=True)
34
+
35
+ def create_backup(self, description: str = "manual_backup"):
36
+ """Create a timestamped backup of current app state"""
37
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
38
+ backup_name = f"{description}_{timestamp}"
39
+
40
+ # Backup main files
41
+ files_to_backup = [
42
+ "streamlit_app.py",
43
+ ".streamlit/secrets.toml",
44
+ "requirements.txt"
45
+ ]
46
+
47
+ backup_path = self.backup_dir / backup_name
48
+ backup_path.mkdir(exist_ok=True)
49
+
50
+ for file_path in files_to_backup:
51
+ if os.path.exists(file_path):
52
+ subprocess.run(["cp", file_path, str(backup_path / os.path.basename(file_path))])
53
+
54
+ logger.info(f"Backup created: {backup_name}")
55
+ return backup_name
56
+
57
+ def restore_backup(self, backup_name: str):
58
+ """Restore from a backup"""
59
+ backup_path = self.backup_dir / backup_name
60
+
61
+ if not backup_path.exists():
62
+ logger.error(f"Backup {backup_name} not found")
63
+ return False
64
+
65
+ # Restore files
66
+ files_to_restore = [
67
+ "streamlit_app.py",
68
+ ".streamlit/secrets.toml",
69
+ "requirements.txt"
70
+ ]
71
+
72
+ for file_path in files_to_restore:
73
+ backup_file = backup_path / os.path.basename(file_path)
74
+ if backup_file.exists():
75
+ subprocess.run(["cp", str(backup_file), file_path])
76
+
77
+ logger.info(f"Restored from backup: {backup_name}")
78
+ return True
79
+
80
+ def test_app_startup(self) -> bool:
81
+ """Test if the app starts without errors"""
82
+ try:
83
+ # Check if app is already running
84
+ response = requests.get(self.app_url, timeout=5)
85
+ if response.status_code == 200:
86
+ logger.info("✅ App is running and accessible")
87
+ return True
88
+ else:
89
+ logger.error(f"❌ App returned status code: {response.status_code}")
90
+ return False
91
+ except requests.exceptions.RequestException as e:
92
+ logger.error(f"❌ App startup test failed: {e}")
93
+ return False
94
+
95
+ def test_ui_elements(self) -> Dict[str, bool]:
96
+ """Test key UI elements are present"""
97
+ results = {}
98
+ try:
99
+ response = requests.get(self.app_url, timeout=10)
100
+ content = response.text.lower()
101
+
102
+ # Test for key UI elements
103
+ ui_tests = {
104
+ "title_present": "care count" in content,
105
+ "signin_form": "sign in" in content or "email" in content,
106
+ "camera_input": "camera" in content or "webcam" in content,
107
+ "file_upload": "upload" in content or "file" in content,
108
+ "visit_management": "visit" in content,
109
+ "item_logging": "item" in content,
110
+ "css_styling": "style" in content or "css" in content
111
+ }
112
+
113
+ for test_name, result in ui_tests.items():
114
+ results[test_name] = result
115
+ status = "✅" if result else "❌"
116
+ logger.info(f"{status} {test_name}: {result}")
117
+
118
+ return results
119
+
120
+ except Exception as e:
121
+ logger.error(f"❌ UI elements test failed: {e}")
122
+ return {}
123
+
124
+ def test_responsive_design(self) -> bool:
125
+ """Test if the app has responsive design elements"""
126
+ try:
127
+ response = requests.get(self.app_url, timeout=10)
128
+ content = response.text
129
+
130
+ # Check for responsive design indicators
131
+ responsive_indicators = [
132
+ "container" in content,
133
+ "column" in content,
134
+ "responsive" in content,
135
+ "mobile" in content
136
+ ]
137
+
138
+ responsive_score = sum(responsive_indicators) / len(responsive_indicators)
139
+ is_responsive = responsive_score >= 0.5
140
+
141
+ logger.info(f"📱 Responsive design score: {responsive_score:.2f} ({'✅' if is_responsive else '❌'})")
142
+ return is_responsive
143
+
144
+ except Exception as e:
145
+ logger.error(f"❌ Responsive design test failed: {e}")
146
+ return False
147
+
148
+ def test_performance(self) -> Dict[str, float]:
149
+ """Test app performance metrics"""
150
+ try:
151
+ start_time = time.time()
152
+ response = requests.get(self.app_url, timeout=30)
153
+ load_time = time.time() - start_time
154
+
155
+ results = {
156
+ "load_time": load_time,
157
+ "status_code": response.status_code,
158
+ "content_size": len(response.content)
159
+ }
160
+
161
+ logger.info(f"⚡ Load time: {load_time:.2f}s")
162
+ logger.info(f"📊 Content size: {len(response.content)} bytes")
163
+
164
+ return results
165
+
166
+ except Exception as e:
167
+ logger.error(f"❌ Performance test failed: {e}")
168
+ return {}
169
+
170
+ def run_all_tests(self) -> Dict[str, Any]:
171
+ """Run all tests and return comprehensive results"""
172
+ logger.info("🧪 Starting comprehensive app testing...")
173
+
174
+ # Create backup before testing
175
+ backup_name = self.create_backup("pre_test_backup")
176
+
177
+ test_results = {
178
+ "timestamp": datetime.now().isoformat(),
179
+ "backup_created": backup_name,
180
+ "startup_test": self.test_app_startup(),
181
+ "ui_elements": self.test_ui_elements(),
182
+ "responsive_design": self.test_responsive_design(),
183
+ "performance": self.test_performance()
184
+ }
185
+
186
+ # Calculate overall score
187
+ ui_score = sum(test_results["ui_elements"].values()) / len(test_results["ui_elements"]) if test_results["ui_elements"] else 0
188
+ overall_score = (
189
+ (1 if test_results["startup_test"] else 0) +
190
+ ui_score +
191
+ (1 if test_results["responsive_design"] else 0)
192
+ ) / 3
193
+
194
+ test_results["overall_score"] = overall_score
195
+ test_results["status"] = "PASS" if overall_score >= 0.8 else "FAIL"
196
+
197
+ logger.info(f"🎯 Overall test score: {overall_score:.2f} ({test_results['status']})")
198
+
199
+ return test_results
200
+
201
+ def main():
202
+ """Main testing function"""
203
+ tester = CareCountTester()
204
+
205
+ if len(sys.argv) > 1:
206
+ command = sys.argv[1]
207
+
208
+ if command == "backup":
209
+ description = sys.argv[2] if len(sys.argv) > 2 else "manual_backup"
210
+ tester.create_backup(description)
211
+ elif command == "restore":
212
+ if len(sys.argv) > 2:
213
+ tester.restore_backup(sys.argv[2])
214
+ else:
215
+ print("Usage: python test_app.py restore <backup_name>")
216
+ elif command == "test":
217
+ results = tester.run_all_tests()
218
+ print(f"\n📋 Test Results Summary:")
219
+ print(f"Status: {results['status']}")
220
+ print(f"Score: {results['overall_score']:.2f}")
221
+ print(f"Backup: {results['backup_created']}")
222
+ else:
223
+ print("Available commands: backup, restore, test")
224
+ else:
225
+ # Run all tests by default
226
+ results = tester.run_all_tests()
227
+ print(f"\n📋 Test Results Summary:")
228
+ print(f"Status: {results['status']}")
229
+ print(f"Score: {results['overall_score']:.2f}")
230
+ print(f"Backup: {results['backup_created']}")
231
+
232
+ if __name__ == "__main__":
233
+ main()
backups/pre_modern_deployment_20250923_203628/ui_improvements.py ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Care Count UI/UX Improvements Module
3
+ Industry-standard UI components and styling
4
+ """
5
+
6
+ import streamlit as st
7
+ from typing import Dict, Any, Optional
8
+ import base64
9
+ from pathlib import Path
10
+
11
+ class ModernUIComponents:
12
+ """Modern UI components for Care Count app"""
13
+
14
+ @staticmethod
15
+ def get_modern_css() -> str:
16
+ """Return modern, industry-standard CSS"""
17
+ return """
18
+ <style>
19
+ /* Modern Design System - Laurier University Inspired */
20
+ :root {
21
+ /* Color Palette - Laurier University Theme */
22
+ --primary-purple: #6b46c1; /* Laurier purple - more vibrant */
23
+ --primary-gold: #fbbf24; /* Laurier gold - warmer tone */
24
+ --primary-dark: #1e1b4b; /* Deep purple background */
25
+ --secondary-dark: #312e81; /* Slightly lighter purple */
26
+ --accent-blue: #3b82f6;
27
+ --accent-green: #10b981;
28
+ --accent-red: #ef4444;
29
+ --accent-orange: #f59e0b;
30
+
31
+ /* Neutral Colors */
32
+ --gray-50: #f9fafb;
33
+ --gray-100: #f3f4f6;
34
+ --gray-200: #e5e7eb;
35
+ --gray-300: #d1d5db;
36
+ --gray-400: #9ca3af;
37
+ --gray-500: #6b7280;
38
+ --gray-600: #4b5563;
39
+ --gray-700: #374151;
40
+ --gray-800: #1f2937;
41
+ --gray-900: #111827;
42
+
43
+ /* Typography */
44
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ --font-size-xs: 0.75rem;
46
+ --font-size-sm: 0.875rem;
47
+ --font-size-base: 1rem;
48
+ --font-size-lg: 1.125rem;
49
+ --font-size-xl: 1.25rem;
50
+ --font-size-2xl: 1.5rem;
51
+ --font-size-3xl: 1.875rem;
52
+
53
+ /* Spacing */
54
+ --space-1: 0.25rem;
55
+ --space-2: 0.5rem;
56
+ --space-3: 0.75rem;
57
+ --space-4: 1rem;
58
+ --space-5: 1.25rem;
59
+ --space-6: 1.5rem;
60
+ --space-8: 2rem;
61
+ --space-10: 2.5rem;
62
+ --space-12: 3rem;
63
+
64
+ /* Border Radius */
65
+ --radius-sm: 0.375rem;
66
+ --radius-md: 0.5rem;
67
+ --radius-lg: 0.75rem;
68
+ --radius-xl: 1rem;
69
+ --radius-2xl: 1.5rem;
70
+
71
+ /* Shadows */
72
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
73
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
74
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
75
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
76
+ }
77
+
78
+ /* Global Styles */
79
+ * {
80
+ box-sizing: border-box;
81
+ }
82
+
83
+ body {
84
+ font-family: var(--font-family);
85
+ background: var(--gray-50);
86
+ color: var(--gray-800);
87
+ line-height: 1.6;
88
+ }
89
+
90
+ /* Streamlit Overrides */
91
+ .main .block-container {
92
+ padding: var(--space-6) var(--space-4);
93
+ max-width: 1200px;
94
+ background: var(--gray-50);
95
+ }
96
+
97
+ .stApp {
98
+ background: var(--gray-50);
99
+ }
100
+
101
+ /* Typography */
102
+ h1, h2, h3, h4, h5, h6 {
103
+ font-weight: 700;
104
+ letter-spacing: -0.025em;
105
+ color: var(--gray-800);
106
+ }
107
+
108
+ h1 {
109
+ font-size: var(--font-size-3xl);
110
+ margin-bottom: var(--space-6);
111
+ }
112
+
113
+ h2 {
114
+ font-size: var(--font-size-2xl);
115
+ margin-bottom: var(--space-4);
116
+ }
117
+
118
+ h3 {
119
+ font-size: var(--font-size-xl);
120
+ margin-bottom: var(--space-3);
121
+ }
122
+
123
+ /* Modern Cards - Laurier Style */
124
+ .modern-card {
125
+ background: white;
126
+ border: 1px solid var(--gray-200);
127
+ border-radius: var(--radius-xl);
128
+ padding: var(--space-6);
129
+ margin-bottom: var(--space-4);
130
+ box-shadow: var(--shadow-md);
131
+ transition: all 0.2s ease;
132
+ color: var(--gray-800);
133
+ }
134
+
135
+ .modern-card:hover {
136
+ border-color: var(--primary-purple);
137
+ box-shadow: var(--shadow-lg);
138
+ transform: translateY(-2px);
139
+ }
140
+
141
+ /* Status Cards */
142
+ .status-grid {
143
+ display: grid;
144
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
145
+ gap: var(--space-4);
146
+ margin-bottom: var(--space-6);
147
+ }
148
+
149
+ .status-card {
150
+ background: white;
151
+ border: 1px solid var(--gray-200);
152
+ border-radius: var(--radius-lg);
153
+ padding: var(--space-5);
154
+ text-align: center;
155
+ transition: all 0.2s ease;
156
+ box-shadow: var(--shadow-sm);
157
+ }
158
+
159
+ .status-card:hover {
160
+ transform: translateY(-2px);
161
+ box-shadow: var(--shadow-md);
162
+ border-color: var(--primary-purple);
163
+ }
164
+
165
+ .status-card h4 {
166
+ margin: 0 0 var(--space-2) 0;
167
+ font-size: var(--font-size-sm);
168
+ color: var(--gray-600);
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.05em;
171
+ font-weight: 600;
172
+ }
173
+
174
+ .status-card .value {
175
+ font-size: var(--font-size-2xl);
176
+ font-weight: 800;
177
+ color: var(--primary-purple);
178
+ margin: 0;
179
+ }
180
+
181
+ .status-card .subtitle {
182
+ font-size: var(--font-size-xs);
183
+ color: var(--gray-500);
184
+ margin: var(--space-1) 0 0 0;
185
+ }
186
+
187
+ /* Modern Buttons */
188
+ .modern-btn {
189
+ background: var(--primary-purple);
190
+ color: white;
191
+ border: none;
192
+ border-radius: var(--radius-md);
193
+ padding: var(--space-3) var(--space-6);
194
+ font-weight: 600;
195
+ font-size: var(--font-size-sm);
196
+ cursor: pointer;
197
+ transition: all 0.2s ease;
198
+ box-shadow: var(--shadow-sm);
199
+ }
200
+
201
+ .modern-btn:hover {
202
+ background: #5b21b6;
203
+ transform: translateY(-1px);
204
+ box-shadow: var(--shadow-md);
205
+ }
206
+
207
+ .modern-btn:active {
208
+ transform: translateY(0);
209
+ }
210
+
211
+ .modern-btn-secondary {
212
+ background: var(--gray-700);
213
+ color: var(--gray-100);
214
+ }
215
+
216
+ .modern-btn-secondary:hover {
217
+ background: var(--gray-600);
218
+ }
219
+
220
+ .modern-btn-success {
221
+ background: var(--accent-green);
222
+ }
223
+
224
+ .modern-btn-success:hover {
225
+ background: #059669;
226
+ }
227
+
228
+ .modern-btn-danger {
229
+ background: var(--accent-red);
230
+ }
231
+
232
+ .modern-btn-danger:hover {
233
+ background: #dc2626;
234
+ }
235
+
236
+ /* Form Elements */
237
+ .modern-input {
238
+ background: white;
239
+ border: 1px solid var(--gray-300);
240
+ border-radius: var(--radius-md);
241
+ padding: var(--space-3);
242
+ color: var(--gray-800);
243
+ font-size: var(--font-size-sm);
244
+ transition: all 0.2s ease;
245
+ }
246
+
247
+ .modern-input:focus {
248
+ outline: none;
249
+ border-color: var(--primary-purple);
250
+ box-shadow: 0 0 0 3px rgb(107 70 193 / 0.1);
251
+ }
252
+
253
+ /* Hero Section - Laurier Style */
254
+ .hero-section {
255
+ background: var(--primary-purple);
256
+ border-radius: var(--radius-2xl);
257
+ padding: var(--space-10);
258
+ margin-bottom: var(--space-8);
259
+ text-align: left;
260
+ color: white;
261
+ box-shadow: var(--shadow-xl);
262
+ position: relative;
263
+ overflow: hidden;
264
+ }
265
+
266
+ .hero-section::before {
267
+ content: '';
268
+ position: absolute;
269
+ top: 0;
270
+ right: 0;
271
+ width: 40%;
272
+ height: 100%;
273
+ background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
274
+ border-radius: 0 var(--radius-2xl) var(--radius-2xl) 0;
275
+ }
276
+
277
+ .hero-section h1 {
278
+ color: white;
279
+ margin-bottom: var(--space-4);
280
+ }
281
+
282
+ .hero-section p {
283
+ font-size: var(--font-size-lg);
284
+ opacity: 0.9;
285
+ margin: 0;
286
+ }
287
+
288
+ /* Progress Indicators */
289
+ .progress-bar {
290
+ background: var(--gray-700);
291
+ border-radius: var(--radius-lg);
292
+ height: 8px;
293
+ overflow: hidden;
294
+ margin: var(--space-2) 0;
295
+ }
296
+
297
+ .progress-fill {
298
+ background: linear-gradient(90deg, var(--primary-purple), var(--primary-gold));
299
+ height: 100%;
300
+ transition: width 0.3s ease;
301
+ }
302
+
303
+ /* Badges */
304
+ .badge {
305
+ display: inline-block;
306
+ padding: var(--space-1) var(--space-2);
307
+ border-radius: var(--radius-sm);
308
+ font-size: var(--font-size-xs);
309
+ font-weight: 600;
310
+ text-transform: uppercase;
311
+ letter-spacing: 0.05em;
312
+ }
313
+
314
+ .badge-success {
315
+ background: var(--accent-green);
316
+ color: white;
317
+ }
318
+
319
+ .badge-warning {
320
+ background: var(--accent-orange);
321
+ color: white;
322
+ }
323
+
324
+ .badge-danger {
325
+ background: var(--accent-red);
326
+ color: white;
327
+ }
328
+
329
+ .badge-info {
330
+ background: var(--accent-blue);
331
+ color: white;
332
+ }
333
+
334
+ /* Responsive Design */
335
+ @media (max-width: 768px) {
336
+ .main .block-container {
337
+ padding: var(--space-4) var(--space-2);
338
+ }
339
+
340
+ .status-grid {
341
+ grid-template-columns: 1fr;
342
+ }
343
+
344
+ .hero-section {
345
+ padding: var(--space-6);
346
+ }
347
+
348
+ h1 {
349
+ font-size: var(--font-size-2xl);
350
+ }
351
+ }
352
+
353
+ /* Loading States */
354
+ .loading {
355
+ display: inline-block;
356
+ width: 20px;
357
+ height: 20px;
358
+ border: 3px solid var(--gray-600);
359
+ border-radius: 50%;
360
+ border-top-color: var(--primary-purple);
361
+ animation: spin 1s ease-in-out infinite;
362
+ }
363
+
364
+ @keyframes spin {
365
+ to { transform: rotate(360deg); }
366
+ }
367
+
368
+ /* Animations */
369
+ .fade-in {
370
+ animation: fadeIn 0.5s ease-in;
371
+ }
372
+
373
+ @keyframes fadeIn {
374
+ from { opacity: 0; transform: translateY(20px); }
375
+ to { opacity: 1; transform: translateY(0); }
376
+ }
377
+
378
+ .slide-in {
379
+ animation: slideIn 0.3s ease-out;
380
+ }
381
+
382
+ @keyframes slideIn {
383
+ from { transform: translateX(-100%); }
384
+ to { transform: translateX(0); }
385
+ }
386
+
387
+ /* Accessibility */
388
+ .sr-only {
389
+ position: absolute;
390
+ width: 1px;
391
+ height: 1px;
392
+ padding: 0;
393
+ margin: -1px;
394
+ overflow: hidden;
395
+ clip: rect(0, 0, 0, 0);
396
+ white-space: nowrap;
397
+ border: 0;
398
+ }
399
+
400
+ /* Focus states for accessibility */
401
+ button:focus,
402
+ input:focus,
403
+ select:focus,
404
+ textarea:focus {
405
+ outline: 2px solid var(--primary-purple);
406
+ outline-offset: 2px;
407
+ }
408
+ </style>
409
+ """
410
+
411
+ @staticmethod
412
+ def create_hero_section(title: str, subtitle: str, user_email: str = None) -> str:
413
+ """Create a Laurier-style hero section"""
414
+ if user_email:
415
+ return f"""
416
+ <div class="hero-section fade-in">
417
+ <div style="position: relative; z-index: 2;">
418
+ <h1 style="font-size: 2.5rem; margin-bottom: var(--space-4); color: white;">💜💛 {title}</h1>
419
+ <p style="font-size: 1.25rem; margin-bottom: var(--space-6); color: white; opacity: 0.9;">{subtitle}</p>
420
+ <div style="margin-top: var(--space-6); padding: var(--space-5); background: rgba(255,255,255,0.15); border-radius: var(--radius-lg); backdrop-filter: blur(10px);">
421
+ <div style="font-size: var(--font-size-sm); opacity: 0.8; color: white;">Welcome,</div>
422
+ <div style="font-weight: 800; font-size: var(--font-size-xl); margin: var(--space-2) 0; color: white;">{user_email}</div>
423
+ <div style="font-size: var(--font-size-sm); opacity: 0.8; color: white;">Thank you for showing up for the community today. 💜💛</div>
424
+ </div>
425
+ </div>
426
+ </div>
427
+ """
428
+ else:
429
+ return f"""
430
+ <div class="hero-section fade-in">
431
+ <div style="position: relative; z-index: 2;">
432
+ <h1 style="font-size: 2.5rem; margin-bottom: var(--space-4); color: white;">💜💛 {title}</h1>
433
+ <p style="font-size: 1.25rem; color: white; opacity: 0.9;">{subtitle}</p>
434
+ </div>
435
+ </div>
436
+ """
437
+
438
+ @staticmethod
439
+ def create_status_cards(data: Dict[str, Any]) -> str:
440
+ """Create modern status cards"""
441
+ cards_html = '<div class="status-grid">'
442
+
443
+ for key, value in data.items():
444
+ if key == "shift_active":
445
+ cards_html += f"""
446
+ <div class="status-card slide-in">
447
+ <h4>Shift Active</h4>
448
+ <div class="value">{value}</div>
449
+ <div class="subtitle">since you signed in</div>
450
+ </div>
451
+ """
452
+ elif key == "items_today":
453
+ cards_html += f"""
454
+ <div class="status-card slide-in">
455
+ <h4>Items Logged Today</h4>
456
+ <div class="value">{value}</div>
457
+ <div class="subtitle">items processed</div>
458
+ </div>
459
+ """
460
+ elif key == "lifetime_hours":
461
+ cards_html += f"""
462
+ <div class="status-card slide-in">
463
+ <h4>Lifetime Hours</h4>
464
+ <div class="value">{value}</div>
465
+ <div class="subtitle">volunteer hours</div>
466
+ </div>
467
+ """
468
+
469
+ cards_html += '</div>'
470
+ return cards_html
471
+
472
+ @staticmethod
473
+ def create_modern_form_section(title: str, description: str = None) -> str:
474
+ """Create a modern form section header"""
475
+ desc_html = f'<p style="color: var(--gray-400); margin-bottom: var(--space-4);">{description}</p>' if description else ''
476
+ return f"""
477
+ <div class="modern-card fade-in">
478
+ <h3>{title}</h3>
479
+ {desc_html}
480
+ """
481
+
482
+ @staticmethod
483
+ def create_progress_indicator(current: int, total: int, label: str) -> str:
484
+ """Create a modern progress indicator"""
485
+ percentage = (current / total * 100) if total > 0 else 0
486
+ return f"""
487
+ <div style="margin: var(--space-4) 0;">
488
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2);">
489
+ <span style="font-weight: 600; color: var(--gray-300);">{label}</span>
490
+ <span style="font-weight: 700; color: var(--primary-gold);">{current}/{total}</span>
491
+ </div>
492
+ <div class="progress-bar">
493
+ <div class="progress-fill" style="width: {percentage}%;"></div>
494
+ </div>
495
+ </div>
496
+ """
497
+
498
+ @staticmethod
499
+ def create_badge(text: str, variant: str = "info") -> str:
500
+ """Create a modern badge"""
501
+ return f'<span class="badge badge-{variant}">{text}</span>'
502
+
503
+ @staticmethod
504
+ def create_loading_spinner() -> str:
505
+ """Create a loading spinner"""
506
+ return '<div class="loading"></div>'
507
+
508
+ def apply_modern_ui():
509
+ """Apply modern UI styling to the Streamlit app"""
510
+ st.markdown(ModernUIComponents.get_modern_css(), unsafe_allow_html=True)
511
+
512
+ def create_modern_layout():
513
+ """Create a modern layout structure"""
514
+ return {
515
+ "container_style": "max-width: 1200px; margin: 0 auto;",
516
+ "sidebar_style": "background: var(--secondary-dark); border-right: 1px solid var(--gray-700);",
517
+ "main_style": "background: var(--primary-dark); padding: var(--space-6);"
518
+ }
backups/pre_modern_deployment_20250923_205210/backup_metadata.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backup_name": "pre_modern_deployment_20250923_205210",
3
+ "timestamp": "20250923_205210",
4
+ "description": "pre_modern_deployment",
5
+ "files_backed_up": [
6
+ "streamlit_app.py",
7
+ "streamlit_app_modern.py",
8
+ ".streamlit/secrets.toml",
9
+ "requirements.txt",
10
+ "ui_improvements.py",
11
+ "test_app.py"
12
+ ],
13
+ "git_status": {
14
+ "modified_files": [
15
+ "M requirements.txt",
16
+ " M streamlit_app.py",
17
+ "?? .streamlit/",
18
+ "?? .venv/",
19
+ "?? COLOR_SCHEME_UPDATE.md",
20
+ "?? DEPLOYMENT_QUICK_REFERENCE.md",
21
+ "?? UI_IMPROVEMENTS_GUIDE.md",
22
+ "?? __pycache__/",
23
+ "?? backups/",
24
+ "?? care_count.log",
25
+ "?? deploy_modern.py",
26
+ "?? deployment.log",
27
+ "?? streamlit_app_backup_20250923_195853.py",
28
+ "?? streamlit_app_modern.py",
29
+ "?? test_app.py",
30
+ "?? test_results.log",
31
+ "?? ui_improvements.py"
32
+ ],
33
+ "return_code": 0
34
+ }
35
+ }
backups/pre_modern_deployment_20250923_205210/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/pre_modern_deployment_20250923_205210/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/pre_modern_deployment_20250923_205210/streamlit_app.py ADDED
@@ -0,0 +1,890 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": json.dumps(details) if isinstance(details, dict) else str(details)
135
+ }
136
+
137
+ # Log to file
138
+ if level == "error":
139
+ logger.error(f"Event: {action} by {actor} - {details}")
140
+ elif level == "warning":
141
+ logger.warning(f"Event: {action} by {actor} - {details}")
142
+ else:
143
+ logger.info(f"Event: {action} by {actor} - {details}")
144
+
145
+ # Log to database (without level field to avoid schema issues)
146
+ try:
147
+ sb.table("events").insert(log_data).execute()
148
+ except Exception as e:
149
+ logger.warning(f"Failed to log event to database (continuing): {e}")
150
+
151
+ # ------------------------ Modern Authentication UI ------------------------
152
+ def auth_block() -> tuple[bool, Optional[str]]:
153
+ """Enhanced authentication with modern UI"""
154
+ if "auth_email" not in st.session_state:
155
+ st.session_state["auth_email"] = None
156
+ if "user_email" in st.session_state:
157
+ return True, st.session_state["user_email"]
158
+
159
+ # Modern hero section for login
160
+ st.markdown(ModernUIComponents.create_hero_section(
161
+ "Care Count",
162
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
163
+ ), unsafe_allow_html=True)
164
+
165
+ # Modern login form
166
+ with st.container():
167
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
168
+ st.subheader("🔐 Sign In")
169
+ st.markdown("Enter your email to receive a secure login code.")
170
+
171
+ with st.form("otp_request", clear_on_submit=False):
172
+ email = st.text_input(
173
+ "Email Address",
174
+ value=st.session_state.get("auth_email") or "",
175
+ placeholder="your.email@example.com",
176
+ help="Enter your email address to receive a secure 6-digit login code. This code will be sent to your email and is valid for 10 minutes."
177
+ )
178
+
179
+ col1, col2, col3 = st.columns([1, 2, 1])
180
+ with col2:
181
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
182
+
183
+ if send:
184
+ if not email or "@" not in email:
185
+ st.error("Please enter a valid email address.")
186
+ else:
187
+ try:
188
+ with st.spinner("Sending login code..."):
189
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
190
+ st.session_state["auth_email"] = email
191
+ st.success("✅ Login code sent! Check your email.")
192
+ log_event("otp_requested", email, {"method": "email"})
193
+ except Exception as e:
194
+ st.error(f"❌ Could not send code: {e}")
195
+ log_event("otp_failed", email, {"error": str(e)}, "error")
196
+
197
+ st.markdown('</div>', unsafe_allow_html=True)
198
+
199
+ # OTP verification form
200
+ if st.session_state.get("auth_email"):
201
+ with st.container():
202
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
203
+ st.subheader("🔢 Verify Code")
204
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
205
+
206
+ with st.form("otp_verify", clear_on_submit=True):
207
+ code = st.text_input(
208
+ "Verification Code",
209
+ max_chars=6,
210
+ placeholder="123456",
211
+ help="Enter the 6-digit verification code that was sent to your email. Check your spam folder if you don't see it in your inbox."
212
+ )
213
+
214
+ col1, col2, col3 = st.columns([1, 2, 1])
215
+ with col2:
216
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
217
+
218
+ if ok:
219
+ if len(code) != 6 or not code.isdigit():
220
+ st.error("Please enter a valid 6-digit code.")
221
+ else:
222
+ try:
223
+ with st.spinner("Verifying code..."):
224
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
225
+ if res and res.user:
226
+ email = st.session_state["auth_email"]
227
+
228
+ # Enhanced volunteer upsert
229
+ volunteer_data = {
230
+ "email": email,
231
+ "last_login_at": datetime.utcnow().isoformat(),
232
+ "shift_started_at": datetime.utcnow().isoformat(),
233
+ "shift_ended_at": None,
234
+ "login_count": 1 # Track login frequency
235
+ }
236
+
237
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
238
+
239
+ st.session_state["user_email"] = email
240
+ st.session_state["shift_started"] = True
241
+ st.session_state["last_activity_at"] = local_now()
242
+
243
+ log_event("login_success", email, {"method": "otp"})
244
+ st.success("🎉 Welcome back! Your shift has started.")
245
+ st.balloons()
246
+ return True, email
247
+ except Exception as e:
248
+ st.error(f"❌ Verification failed: {e}")
249
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
250
+
251
+ st.markdown('</div>', unsafe_allow_html=True)
252
+
253
+ return False, None
254
+
255
+ def end_shift(email: str, reason: str):
256
+ """Enhanced shift ending with better logging"""
257
+ try:
258
+ end_time = datetime.utcnow().isoformat()
259
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
260
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
261
+ except Exception as e:
262
+ logger.error(f"Failed to end shift for {email}: {e}")
263
+
264
+ def guard_cutoff_and_idle(email: str):
265
+ """Enhanced session management with better UX"""
266
+ now = local_now()
267
+ last = st.session_state.get("last_activity_at")
268
+
269
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
270
+ end_shift(email, "inactivity")
271
+ st.session_state.clear()
272
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
273
+ st.stop()
274
+
275
+ st.session_state["last_activity_at"] = now
276
+
277
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
278
+ if now >= cutoff:
279
+ end_shift(email, "cutoff_8pm")
280
+ st.session_state.clear()
281
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
282
+ st.stop()
283
+
284
+ # ------------------------ Main App Flow ------------------------
285
+ def main():
286
+ """Main application flow with modern UI"""
287
+
288
+ # Authentication
289
+ signed_in, user_email = auth_block()
290
+ if not signed_in:
291
+ st.stop()
292
+
293
+ guard_cutoff_and_idle(user_email)
294
+
295
+ # Modern welcome section
296
+ st.markdown(ModernUIComponents.create_hero_section(
297
+ "Care Count Dashboard",
298
+ "Manage visits, track items, and make a difference in your community.",
299
+ user_email
300
+ ), unsafe_allow_html=True)
301
+
302
+ # Enhanced status cards
303
+ vrow = fetch_volunteer_row(user_email) or {}
304
+ shift_started_at = vrow.get("shift_started_at")
305
+ lifetime_hours = vrow.get("total_hours", 0)
306
+
307
+ mins_active = 0
308
+ try:
309
+ if shift_started_at:
310
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
311
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
312
+ except Exception:
313
+ pass
314
+
315
+ status_data = {
316
+ "shift_active": f"{mins_active} min",
317
+ "items_today": items_today(user_email),
318
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
319
+ }
320
+
321
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
322
+
323
+ # Modern sign-out button
324
+ col1, col2, col3 = st.columns([1, 1, 1])
325
+ with col2:
326
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
327
+ end_shift(user_email, "manual")
328
+ st.session_state.clear()
329
+ st.success("✅ Signed out successfully. See you next time!")
330
+ st.rerun()
331
+
332
+ # Enhanced visit management section
333
+ st.markdown(ModernUIComponents.create_modern_form_section(
334
+ "🪪 Visit Management",
335
+ "Start and manage student visits with unique tracking codes"
336
+ ), unsafe_allow_html=True)
337
+
338
+ active_visit = st.session_state.get("active_visit")
339
+
340
+ col1, col2, col3 = st.columns([1, 1, 1])
341
+
342
+ with col1:
343
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary", help="Start a new student visit session. This will create a unique visit code for tracking items."):
344
+ try:
345
+ with st.spinner("Creating visit..."):
346
+ payload = {
347
+ "visit_code": fallback_visit_code(),
348
+ "started_at": datetime.utcnow().isoformat(),
349
+ "ended_at": None,
350
+ "created_by": user_email
351
+ }
352
+ v = sb.table("visits").insert(payload).execute().data[0]
353
+ if not v.get("visit_code"):
354
+ v["visit_code"] = payload["visit_code"]
355
+ st.session_state["active_visit"] = v
356
+ st.success(f"✅ Visit #{v['id']} started")
357
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
358
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
359
+ st.rerun()
360
+ except Exception as e:
361
+ st.error(f"❌ Could not start visit: {e}")
362
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
363
+
364
+ with col2:
365
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary", help="End the current student visit session. This will finalize the visit and prepare for the next student."):
366
+ try:
367
+ with st.spinner("Ending visit..."):
368
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
369
+ .eq("id", active_visit["id"]).execute()
370
+ st.success("✅ Visit completed successfully")
371
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
372
+ st.session_state.pop("active_visit", None)
373
+ st.rerun()
374
+ except Exception as e:
375
+ st.error(f"❌ Could not end visit: {e}")
376
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
377
+
378
+ with col3:
379
+ if st.session_state.get("active_visit"):
380
+ v = st.session_state["active_visit"]
381
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
382
+ else:
383
+ st.info("No active visit")
384
+
385
+ st.markdown('</div>', unsafe_allow_html=True)
386
+
387
+ # Enhanced item identification section
388
+ st.markdown(ModernUIComponents.create_modern_form_section(
389
+ "📸 Item Identification",
390
+ "Use AI-powered image recognition to identify items quickly and accurately"
391
+ ), unsafe_allow_html=True)
392
+
393
+ col1, col2 = st.columns(2)
394
+
395
+ with col1:
396
+ st.subheader("📷 Camera Capture")
397
+ cam = st.camera_input("Take a photo of the item", help="Use your device's camera to take a clear photo of the item. Make sure the item is well-lit and clearly visible in the frame.")
398
+
399
+ with col2:
400
+ st.subheader("📁 File Upload")
401
+ up = st.file_uploader(
402
+ "Upload an image",
403
+ type=["png","jpg","jpeg"],
404
+ help="Upload a photo of the item from your device. Supported formats: PNG, JPG, JPEG. Maximum file size: 200MB."
405
+ )
406
+
407
+ img_file = cam or up
408
+ if img_file:
409
+ try:
410
+ img = Image.open(img_file).convert("RGB")
411
+ st.image(img, use_container_width=True, caption="Captured Image")
412
+
413
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary", help="Use AI to automatically identify the item in the image. This will analyze the photo and suggest an item name."):
414
+ with st.spinner("Analyzing image with AI..."):
415
+ t0 = time.time()
416
+ try:
417
+ pre = preprocess_for_label(img)
418
+ raw = gemma_item_name(_to_png_bytes(pre))
419
+ processing_time = time.time() - t0
420
+
421
+ norm = normalize_item_name(raw)
422
+
423
+ if raw:
424
+ st.success(f"🤖 **AI Detection:** {raw}")
425
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
426
+
427
+ st.session_state["scanned_item_name"] = norm or raw or ""
428
+ st.session_state["last_activity_at"] = local_now()
429
+
430
+ log_event("item_identified", user_email, {
431
+ "raw_name": raw,
432
+ "normalized_name": norm,
433
+ "processing_time": processing_time
434
+ })
435
+
436
+ except Exception as e:
437
+ st.error(f"❌ AI identification failed: {e}")
438
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
439
+ except Exception as e:
440
+ st.error(f"❌ Failed to process image: {e}")
441
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
442
+
443
+ st.markdown('</div>', unsafe_allow_html=True)
444
+
445
+ # Enhanced item logging section
446
+ st.markdown(ModernUIComponents.create_modern_form_section(
447
+ "📬 Item Logging",
448
+ "Log items to the current visit with detailed information"
449
+ ), unsafe_allow_html=True)
450
+
451
+ col1, col2 = st.columns([2, 1])
452
+
453
+ with col1:
454
+ item_name = st.text_input(
455
+ "Item Name",
456
+ value=st.session_state.get("scanned_item_name",""),
457
+ placeholder="Enter item name or use AI detection above",
458
+ help="Enter the name of the item. You can type it manually or use the AI detection feature above to automatically identify it from a photo."
459
+ )
460
+
461
+ with col2:
462
+ quantity = st.number_input(
463
+ "Quantity",
464
+ min_value=1,
465
+ max_value=9999,
466
+ step=1,
467
+ value=1,
468
+ help="Number of items"
469
+ )
470
+
471
+ col3, col4 = st.columns(2)
472
+
473
+ with col3:
474
+ category = st.text_input(
475
+ "Category (optional)",
476
+ placeholder="e.g., Food, Hygiene, Clothing",
477
+ help="Item category for better organization"
478
+ )
479
+
480
+ with col4:
481
+ unit = st.text_input(
482
+ "Unit (optional)",
483
+ placeholder="e.g., 500 mL, 1 L, 250 g",
484
+ help="Unit of measurement"
485
+ )
486
+
487
+ barcode = st.text_input(
488
+ "Barcode (optional)",
489
+ placeholder="Scan or enter barcode",
490
+ help="Product barcode for inventory tracking"
491
+ )
492
+
493
+ save_disabled = not st.session_state.get("active_visit")
494
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary", help="Save the item to the current visit. You must start a visit first before you can log items."):
495
+ v = st.session_state.get("active_visit")
496
+ if not v:
497
+ st.warning("⚠️ Please start a visit first before logging items.")
498
+ else:
499
+ name_clean = clean_text(item_name, 120)
500
+ if not name_clean:
501
+ st.warning("⚠️ Item name is required.")
502
+ else:
503
+ with st.spinner("Saving item..."):
504
+ ts_iso = datetime.utcnow().isoformat()
505
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
506
+
507
+ try:
508
+ ok, msg = try_rpc_ingest(
509
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
510
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
511
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
512
+ )
513
+
514
+ if ok:
515
+ st.success("✅ Item logged successfully!")
516
+ log_event("item_logged", user_email, {
517
+ "visit_id": v["id"],
518
+ "item_name": name_clean,
519
+ "quantity": quantity
520
+ })
521
+ else:
522
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
523
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
524
+ clean_text(category,80), clean_text(unit,40),
525
+ clean_text(barcode,64), ts_iso, ingest_id)
526
+ st.success("✅ Item logged successfully (fallback method)!")
527
+ log_event("item_logged_fallback", user_email, {
528
+ "visit_id": v["id"],
529
+ "item_name": name_clean,
530
+ "quantity": quantity
531
+ })
532
+ except Exception as e:
533
+ try:
534
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
535
+ clean_text(category,80), clean_text(unit,40),
536
+ clean_text(barcode,64), ts_iso, ingest_id)
537
+ st.success("✅ Item logged successfully (fallback method)!")
538
+ log_event("item_logged_fallback", user_email, {
539
+ "visit_id": v["id"],
540
+ "item_name": name_clean,
541
+ "quantity": quantity
542
+ })
543
+ except Exception as e2:
544
+ st.error(f"❌ Failed to log item: {e2}")
545
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
546
+
547
+ st.session_state["last_activity_at"] = local_now()
548
+ st.session_state["scanned_item_name"] = name_clean
549
+ st.rerun()
550
+
551
+ st.markdown('</div>', unsafe_allow_html=True)
552
+
553
+ # Enhanced visit items view
554
+ if st.session_state.get("active_visit"):
555
+ st.markdown(ModernUIComponents.create_modern_form_section(
556
+ "🧾 Current Visit Items",
557
+ "Review and manage items in the current visit"
558
+ ), unsafe_allow_html=True)
559
+
560
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
561
+ if rows:
562
+ df = pd.DataFrame(rows)
563
+
564
+ # Enhanced dataframe display
565
+ st.dataframe(
566
+ df,
567
+ use_container_width=True,
568
+ hide_index=True,
569
+ column_config={
570
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
571
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
572
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
573
+ "category": st.column_config.TextColumn("Category", width="small"),
574
+ "unit": st.column_config.TextColumn("Unit", width="small")
575
+ }
576
+ )
577
+
578
+ # Enhanced delete functionality
579
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
580
+ if rows:
581
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
582
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
583
+
584
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
585
+ item_id = item_options[selected_item]
586
+ try:
587
+ # Try both tables safely
588
+ try:
589
+ delete_item("visit_items_p", int(item_id))
590
+ except Exception:
591
+ delete_item("visit_items", int(item_id))
592
+
593
+ st.success("✅ Item deleted successfully")
594
+ log_event("item_deleted", user_email, {"item_id": item_id})
595
+ st.rerun()
596
+ except Exception as e:
597
+ st.error(f"❌ Failed to delete item: {e}")
598
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
599
+ else:
600
+ st.info("No items to delete")
601
+ else:
602
+ st.info("📝 No items logged for this visit yet.")
603
+
604
+ st.markdown('</div>', unsafe_allow_html=True)
605
+
606
+ # Enhanced analytics section
607
+ st.markdown(ModernUIComponents.create_modern_form_section(
608
+ "📈 Today's Analytics",
609
+ "Real-time insights into today's volunteer activity"
610
+ ), unsafe_allow_html=True)
611
+
612
+ try:
613
+ daily = sb.table("v_daily_activity").select("*").execute().data
614
+ today_str = local_now().strftime("%Y-%m-%d")
615
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
616
+
617
+ if today:
618
+ visits = int(today.get("visits", 0))
619
+ items = int(today.get("items", 0))
620
+
621
+ col1, col2 = st.columns(2)
622
+ with col1:
623
+ st.metric("Total Visits Today", visits, delta=None)
624
+ with col2:
625
+ st.metric("Total Items Processed", items, delta=None)
626
+
627
+ # Progress indicator
628
+ if visits > 0:
629
+ st.markdown(ModernUIComponents.create_progress_indicator(
630
+ items, visits * 10, "Items per Visit Target"
631
+ ), unsafe_allow_html=True)
632
+ else:
633
+ st.info("📊 Analytics data will appear here as activity increases.")
634
+
635
+ except Exception as e:
636
+ st.info("📊 Analytics view will be available once the database view is set up.")
637
+ logger.warning(f"Analytics view not available: {e}")
638
+
639
+ st.markdown('</div>', unsafe_allow_html=True)
640
+
641
+ # Enhanced footer with helpful information
642
+ st.markdown("""
643
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
644
+ <h4>💡 Quick Tips</h4>
645
+ <p style="color: var(--gray-400); margin: 0;">
646
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
647
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
648
+ </p>
649
+ </div>
650
+ """, unsafe_allow_html=True)
651
+
652
+ # ------------------------ Helper Functions (Enhanced) ------------------------
653
+ def fetch_volunteer_row(email: str) -> dict | None:
654
+ """Enhanced volunteer data fetching with error handling"""
655
+ try:
656
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
657
+ except Exception as e:
658
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
659
+ return None
660
+
661
+ def items_today(email: str) -> int:
662
+ """Enhanced item counting with better error handling"""
663
+ try:
664
+ day = local_now().strftime("%Y-%m-%d")
665
+ # Try partitioned table first
666
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
667
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
668
+ .eq("volunteer", email).execute().data
669
+ return len(data or [])
670
+ except Exception:
671
+ try:
672
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
673
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
674
+ .eq("volunteer", email).execute().data
675
+ return len(data or [])
676
+ except Exception as e:
677
+ logger.warning(f"Failed to count items for {email}: {e}")
678
+ return 0
679
+
680
+ def fallback_visit_code() -> str:
681
+ """Enhanced visit code generation with better uniqueness"""
682
+ try:
683
+ day = local_now().strftime("%Y-%m-%d")
684
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
685
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
686
+ seq = len(todays) + 1
687
+ except Exception:
688
+ seq = int(time.time()) % 1000
689
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
690
+
691
+ def _to_png_bytes(img: Image.Image) -> bytes:
692
+ """Convert image to PNG bytes"""
693
+ b = io.BytesIO()
694
+ img.save(b, format="PNG")
695
+ return b.getvalue()
696
+
697
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
698
+ """Enhanced image preprocessing for better AI recognition"""
699
+ img = img.convert("RGB")
700
+ w, h = img.size
701
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
702
+ if scale < 1.0:
703
+ img = img.resize((int(w * scale), int(h * scale)))
704
+ img = ImageOps.autocontrast(img, cutoff=2)
705
+ img = ImageEnhance.Brightness(img).enhance(1.06)
706
+ img = ImageEnhance.Contrast(img).enhance(1.05)
707
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
708
+ return img
709
+
710
+ def gemma_item_name(img_bytes: bytes) -> str:
711
+ """Enhanced AI item identification with better error handling"""
712
+ try:
713
+ if PROVIDER == "nebius":
714
+ if not NEBIUS_API_KEY:
715
+ raise RuntimeError("NEBIUS_API_KEY missing")
716
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
717
+ elif PROVIDER == "featherless":
718
+ if not FEATH_API_KEY:
719
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
720
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
721
+ else:
722
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
723
+ except Exception as e:
724
+ logger.error(f"AI identification failed: {e}")
725
+ raise
726
+
727
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
728
+ """Enhanced API communication with better error handling"""
729
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
730
+ url = f"{base_url.rstrip('/')}/chat/completions"
731
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
732
+ payload = {
733
+ "model": model_id,
734
+ "temperature": 0,
735
+ "messages": [
736
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
737
+ {"role": "user", "content": [
738
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
739
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
740
+ ]}
741
+ ]
742
+ }
743
+
744
+ try:
745
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
746
+ if r.status_code != 200:
747
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
748
+ data = r.json()
749
+ return (data["choices"][0]["message"]["content"] or "").strip()
750
+ except requests.exceptions.Timeout:
751
+ raise RuntimeError("AI service timeout - please try again")
752
+ except requests.exceptions.RequestException as e:
753
+ raise RuntimeError(f"AI service error: {e}")
754
+
755
+ def normalize_item_name(s: str) -> str:
756
+ """Enhanced item name normalization"""
757
+ s = (s or "").strip()
758
+ if not s:
759
+ return ""
760
+
761
+ # Enhanced brand and type recognition
762
+ BRANDS = {
763
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
764
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
765
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
766
+ }
767
+ GENERIC_TYPES = {
768
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
769
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
770
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
771
+ "crackers","cookies","soup","insect killer","spray"
772
+ }
773
+
774
+ low = re.sub(r"[®™]", "", s.lower())
775
+ for b in BRANDS:
776
+ low = low.replace(b, "")
777
+
778
+ chosen = None
779
+ for t in GENERIC_TYPES:
780
+ if t in low:
781
+ chosen = t
782
+ break
783
+
784
+ cleaned = " ".join(low.split())
785
+ return (chosen or cleaned.title())[:120]
786
+
787
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
788
+ """Enhanced text cleaning"""
789
+ if not v:
790
+ return None
791
+ v = re.sub(r"\s+", " ", v).strip()
792
+ return v[:maxlen] if v else None
793
+
794
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
795
+ """Enhanced ID generation for data integrity"""
796
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
797
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
798
+
799
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
800
+ category: Optional[str], unit: Optional[str],
801
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
802
+ """Enhanced RPC ingestion with better error handling"""
803
+ try:
804
+ res = sb.rpc("safe_ingest_visit_item", {
805
+ "p_email": email,
806
+ "p_visit_id": v_id,
807
+ "p_item_name": name,
808
+ "p_qty": qty,
809
+ "p_category": category,
810
+ "p_unit": unit,
811
+ "p_barcode": barcode,
812
+ "p_ts": ts_iso,
813
+ "p_ingest_id": ingest_id
814
+ }).execute()
815
+
816
+ rows = res.data if isinstance(res.data, list) else []
817
+ if rows:
818
+ r0 = rows[0]
819
+ ok = bool(r0.get("ok", False))
820
+ msg = str(r0.get("msg", ""))
821
+ return ok, msg or ("ok" if ok else "failed")
822
+ return True, "ok"
823
+ except Exception as e:
824
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
825
+ raise e
826
+
827
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
828
+ category: Optional[str], unit: Optional[str],
829
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
830
+ """Enhanced fallback insertion with better error handling"""
831
+ payload = {
832
+ "visit_id": v_id,
833
+ "timestamp": ts_iso,
834
+ "volunteer": email,
835
+ "item_name": name,
836
+ "category": category,
837
+ "unit": unit,
838
+ "qty": qty,
839
+ "barcode": barcode,
840
+ "weather_type": None,
841
+ "temp_c": None,
842
+ "ingest_id": ingest_id
843
+ }
844
+
845
+ try:
846
+ sb.table("visit_items_p").insert(payload).execute()
847
+ except Exception:
848
+ # Legacy table fallback
849
+ payload.pop("ingest_id", None)
850
+ sb.table("visit_items").insert(payload).execute()
851
+
852
+ def load_items_for_visit(visit_id: int) -> list[dict]:
853
+ """Enhanced item loading with better error handling"""
854
+ try:
855
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
856
+ .order("timestamp", desc=True).limit(500).execute().data or []
857
+ except Exception:
858
+ try:
859
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
860
+ .order("timestamp", desc=True).limit(500).execute().data or []
861
+ except Exception as e:
862
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
863
+ return []
864
+
865
+ def delete_item(table: str, item_id: int):
866
+ """Enhanced item deletion with better error handling"""
867
+ try:
868
+ sb.table(table).delete().eq("id", item_id).execute()
869
+ except Exception as e:
870
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
871
+ raise e
872
+
873
+ # ------------------------ App Configuration Display ------------------------
874
+ st.sidebar.markdown("### ⚙️ Configuration")
875
+ st.sidebar.info(f"""
876
+ **Provider:** `{PROVIDER}`
877
+ **Model:** `{GEMMA_MODEL}`
878
+ **Timezone:** `{TZ}`
879
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
880
+ **Inactivity:** {INACTIVITY_MIN} min
881
+ """)
882
+
883
+ # ------------------------ Main App Execution ------------------------
884
+ if __name__ == "__main__":
885
+ try:
886
+ main()
887
+ except Exception as e:
888
+ logger.error(f"Application error: {e}")
889
+ st.error(f"❌ Application error: {e}")
890
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/pre_modern_deployment_20250923_205210/streamlit_app_modern.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ))
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ))
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")
backups/pre_modern_deployment_20250923_205210/test_app.py ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Care Count App Testing Framework
4
+ Tests UI/UX changes and backend functionality
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import time
10
+ import logging
11
+ from datetime import datetime
12
+ from typing import Dict, Any, List
13
+ import subprocess
14
+ import requests
15
+ from pathlib import Path
16
+
17
+ # Setup logging
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format='%(asctime)s - %(levelname)s - %(message)s',
21
+ handlers=[
22
+ logging.FileHandler('test_results.log'),
23
+ logging.StreamHandler()
24
+ ]
25
+ )
26
+ logger = logging.getLogger(__name__)
27
+
28
+ class CareCountTester:
29
+ def __init__(self):
30
+ self.test_results = []
31
+ self.app_url = "http://localhost:8501"
32
+ self.backup_dir = Path("backups")
33
+ self.backup_dir.mkdir(exist_ok=True)
34
+
35
+ def create_backup(self, description: str = "manual_backup"):
36
+ """Create a timestamped backup of current app state"""
37
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
38
+ backup_name = f"{description}_{timestamp}"
39
+
40
+ # Backup main files
41
+ files_to_backup = [
42
+ "streamlit_app.py",
43
+ ".streamlit/secrets.toml",
44
+ "requirements.txt"
45
+ ]
46
+
47
+ backup_path = self.backup_dir / backup_name
48
+ backup_path.mkdir(exist_ok=True)
49
+
50
+ for file_path in files_to_backup:
51
+ if os.path.exists(file_path):
52
+ subprocess.run(["cp", file_path, str(backup_path / os.path.basename(file_path))])
53
+
54
+ logger.info(f"Backup created: {backup_name}")
55
+ return backup_name
56
+
57
+ def restore_backup(self, backup_name: str):
58
+ """Restore from a backup"""
59
+ backup_path = self.backup_dir / backup_name
60
+
61
+ if not backup_path.exists():
62
+ logger.error(f"Backup {backup_name} not found")
63
+ return False
64
+
65
+ # Restore files
66
+ files_to_restore = [
67
+ "streamlit_app.py",
68
+ ".streamlit/secrets.toml",
69
+ "requirements.txt"
70
+ ]
71
+
72
+ for file_path in files_to_restore:
73
+ backup_file = backup_path / os.path.basename(file_path)
74
+ if backup_file.exists():
75
+ subprocess.run(["cp", str(backup_file), file_path])
76
+
77
+ logger.info(f"Restored from backup: {backup_name}")
78
+ return True
79
+
80
+ def test_app_startup(self) -> bool:
81
+ """Test if the app starts without errors"""
82
+ try:
83
+ # Check if app is already running
84
+ response = requests.get(self.app_url, timeout=5)
85
+ if response.status_code == 200:
86
+ logger.info("✅ App is running and accessible")
87
+ return True
88
+ else:
89
+ logger.error(f"❌ App returned status code: {response.status_code}")
90
+ return False
91
+ except requests.exceptions.RequestException as e:
92
+ logger.error(f"❌ App startup test failed: {e}")
93
+ return False
94
+
95
+ def test_ui_elements(self) -> Dict[str, bool]:
96
+ """Test key UI elements are present"""
97
+ results = {}
98
+ try:
99
+ response = requests.get(self.app_url, timeout=10)
100
+ content = response.text.lower()
101
+
102
+ # Test for key UI elements
103
+ ui_tests = {
104
+ "title_present": "care count" in content,
105
+ "signin_form": "sign in" in content or "email" in content,
106
+ "camera_input": "camera" in content or "webcam" in content,
107
+ "file_upload": "upload" in content or "file" in content,
108
+ "visit_management": "visit" in content,
109
+ "item_logging": "item" in content,
110
+ "css_styling": "style" in content or "css" in content
111
+ }
112
+
113
+ for test_name, result in ui_tests.items():
114
+ results[test_name] = result
115
+ status = "✅" if result else "❌"
116
+ logger.info(f"{status} {test_name}: {result}")
117
+
118
+ return results
119
+
120
+ except Exception as e:
121
+ logger.error(f"❌ UI elements test failed: {e}")
122
+ return {}
123
+
124
+ def test_responsive_design(self) -> bool:
125
+ """Test if the app has responsive design elements"""
126
+ try:
127
+ response = requests.get(self.app_url, timeout=10)
128
+ content = response.text
129
+
130
+ # Check for responsive design indicators
131
+ responsive_indicators = [
132
+ "container" in content,
133
+ "column" in content,
134
+ "responsive" in content,
135
+ "mobile" in content
136
+ ]
137
+
138
+ responsive_score = sum(responsive_indicators) / len(responsive_indicators)
139
+ is_responsive = responsive_score >= 0.5
140
+
141
+ logger.info(f"📱 Responsive design score: {responsive_score:.2f} ({'✅' if is_responsive else '❌'})")
142
+ return is_responsive
143
+
144
+ except Exception as e:
145
+ logger.error(f"❌ Responsive design test failed: {e}")
146
+ return False
147
+
148
+ def test_performance(self) -> Dict[str, float]:
149
+ """Test app performance metrics"""
150
+ try:
151
+ start_time = time.time()
152
+ response = requests.get(self.app_url, timeout=30)
153
+ load_time = time.time() - start_time
154
+
155
+ results = {
156
+ "load_time": load_time,
157
+ "status_code": response.status_code,
158
+ "content_size": len(response.content)
159
+ }
160
+
161
+ logger.info(f"⚡ Load time: {load_time:.2f}s")
162
+ logger.info(f"📊 Content size: {len(response.content)} bytes")
163
+
164
+ return results
165
+
166
+ except Exception as e:
167
+ logger.error(f"❌ Performance test failed: {e}")
168
+ return {}
169
+
170
+ def run_all_tests(self) -> Dict[str, Any]:
171
+ """Run all tests and return comprehensive results"""
172
+ logger.info("🧪 Starting comprehensive app testing...")
173
+
174
+ # Create backup before testing
175
+ backup_name = self.create_backup("pre_test_backup")
176
+
177
+ test_results = {
178
+ "timestamp": datetime.now().isoformat(),
179
+ "backup_created": backup_name,
180
+ "startup_test": self.test_app_startup(),
181
+ "ui_elements": self.test_ui_elements(),
182
+ "responsive_design": self.test_responsive_design(),
183
+ "performance": self.test_performance()
184
+ }
185
+
186
+ # Calculate overall score
187
+ ui_score = sum(test_results["ui_elements"].values()) / len(test_results["ui_elements"]) if test_results["ui_elements"] else 0
188
+ overall_score = (
189
+ (1 if test_results["startup_test"] else 0) +
190
+ ui_score +
191
+ (1 if test_results["responsive_design"] else 0)
192
+ ) / 3
193
+
194
+ test_results["overall_score"] = overall_score
195
+ test_results["status"] = "PASS" if overall_score >= 0.8 else "FAIL"
196
+
197
+ logger.info(f"🎯 Overall test score: {overall_score:.2f} ({test_results['status']})")
198
+
199
+ return test_results
200
+
201
+ def main():
202
+ """Main testing function"""
203
+ tester = CareCountTester()
204
+
205
+ if len(sys.argv) > 1:
206
+ command = sys.argv[1]
207
+
208
+ if command == "backup":
209
+ description = sys.argv[2] if len(sys.argv) > 2 else "manual_backup"
210
+ tester.create_backup(description)
211
+ elif command == "restore":
212
+ if len(sys.argv) > 2:
213
+ tester.restore_backup(sys.argv[2])
214
+ else:
215
+ print("Usage: python test_app.py restore <backup_name>")
216
+ elif command == "test":
217
+ results = tester.run_all_tests()
218
+ print(f"\n📋 Test Results Summary:")
219
+ print(f"Status: {results['status']}")
220
+ print(f"Score: {results['overall_score']:.2f}")
221
+ print(f"Backup: {results['backup_created']}")
222
+ else:
223
+ print("Available commands: backup, restore, test")
224
+ else:
225
+ # Run all tests by default
226
+ results = tester.run_all_tests()
227
+ print(f"\n📋 Test Results Summary:")
228
+ print(f"Status: {results['status']}")
229
+ print(f"Score: {results['overall_score']:.2f}")
230
+ print(f"Backup: {results['backup_created']}")
231
+
232
+ if __name__ == "__main__":
233
+ main()
backups/pre_modern_deployment_20250923_205210/ui_improvements.py ADDED
@@ -0,0 +1,583 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Care Count UI/UX Improvements Module
3
+ Industry-standard UI components and styling
4
+ """
5
+
6
+ import streamlit as st
7
+ from typing import Dict, Any, Optional
8
+ import base64
9
+ from pathlib import Path
10
+
11
+ class ModernUIComponents:
12
+ """Modern UI components for Care Count app"""
13
+
14
+ @staticmethod
15
+ def get_modern_css() -> str:
16
+ """Return modern, industry-standard CSS"""
17
+ return """
18
+ <style>
19
+ /* Modern Design System - Laurier University Inspired */
20
+ :root {
21
+ /* Color Palette - Laurier University Theme */
22
+ --primary-purple: #6b46c1; /* Laurier purple - more vibrant */
23
+ --primary-gold: #fbbf24; /* Laurier gold - warmer tone */
24
+ --primary-dark: #1e1b4b; /* Deep purple background */
25
+ --secondary-dark: #312e81; /* Slightly lighter purple */
26
+ --accent-blue: #3b82f6;
27
+ --accent-green: #10b981;
28
+ --accent-red: #ef4444;
29
+ --accent-orange: #f59e0b;
30
+
31
+ /* Neutral Colors */
32
+ --gray-50: #f9fafb;
33
+ --gray-100: #f3f4f6;
34
+ --gray-200: #e5e7eb;
35
+ --gray-300: #d1d5db;
36
+ --gray-400: #9ca3af;
37
+ --gray-500: #6b7280;
38
+ --gray-600: #4b5563;
39
+ --gray-700: #374151;
40
+ --gray-800: #1f2937;
41
+ --gray-900: #111827;
42
+
43
+ /* Typography */
44
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ --font-size-xs: 0.75rem;
46
+ --font-size-sm: 0.875rem;
47
+ --font-size-base: 1rem;
48
+ --font-size-lg: 1.125rem;
49
+ --font-size-xl: 1.25rem;
50
+ --font-size-2xl: 1.5rem;
51
+ --font-size-3xl: 1.875rem;
52
+
53
+ /* Spacing */
54
+ --space-1: 0.25rem;
55
+ --space-2: 0.5rem;
56
+ --space-3: 0.75rem;
57
+ --space-4: 1rem;
58
+ --space-5: 1.25rem;
59
+ --space-6: 1.5rem;
60
+ --space-8: 2rem;
61
+ --space-10: 2.5rem;
62
+ --space-12: 3rem;
63
+
64
+ /* Border Radius */
65
+ --radius-sm: 0.375rem;
66
+ --radius-md: 0.5rem;
67
+ --radius-lg: 0.75rem;
68
+ --radius-xl: 1rem;
69
+ --radius-2xl: 1.5rem;
70
+
71
+ /* Shadows */
72
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
73
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
74
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
75
+ --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
76
+ }
77
+
78
+ /* Global Styles */
79
+ * {
80
+ box-sizing: border-box;
81
+ }
82
+
83
+ body {
84
+ font-family: var(--font-family);
85
+ background: var(--gray-50);
86
+ color: var(--gray-800);
87
+ line-height: 1.6;
88
+ }
89
+
90
+ /* Streamlit Overrides */
91
+ .main .block-container {
92
+ padding: var(--space-6) var(--space-4);
93
+ max-width: 1200px;
94
+ background: var(--gray-50);
95
+ }
96
+
97
+ .stApp {
98
+ background: var(--gray-50);
99
+ }
100
+
101
+ /* Typography */
102
+ h1, h2, h3, h4, h5, h6 {
103
+ font-weight: 700;
104
+ letter-spacing: -0.025em;
105
+ color: var(--gray-800);
106
+ }
107
+
108
+ h1 {
109
+ font-size: var(--font-size-3xl);
110
+ margin-bottom: var(--space-6);
111
+ }
112
+
113
+ h2 {
114
+ font-size: var(--font-size-2xl);
115
+ margin-bottom: var(--space-4);
116
+ }
117
+
118
+ h3 {
119
+ font-size: var(--font-size-xl);
120
+ margin-bottom: var(--space-3);
121
+ }
122
+
123
+ /* Modern Cards - Laurier Style */
124
+ .modern-card {
125
+ background: white;
126
+ border: 1px solid var(--gray-200);
127
+ border-radius: var(--radius-xl);
128
+ padding: var(--space-6);
129
+ margin-bottom: var(--space-4);
130
+ box-shadow: var(--shadow-md);
131
+ transition: all 0.2s ease;
132
+ color: var(--gray-800);
133
+ }
134
+
135
+ .modern-card:hover {
136
+ border-color: var(--primary-purple);
137
+ box-shadow: var(--shadow-lg);
138
+ transform: translateY(-2px);
139
+ }
140
+
141
+ .modern-card:hover h1,
142
+ .modern-card:hover h2,
143
+ .modern-card:hover h3,
144
+ .modern-card:hover h4,
145
+ .modern-card:hover h5,
146
+ .modern-card:hover h6,
147
+ .modern-card:hover p,
148
+ .modern-card:hover span,
149
+ .modern-card:hover div {
150
+ color: var(--gray-800) !important;
151
+ }
152
+
153
+ /* Status Cards */
154
+ .status-grid {
155
+ display: grid;
156
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
157
+ gap: var(--space-4);
158
+ margin-bottom: var(--space-6);
159
+ }
160
+
161
+ .status-card {
162
+ background: white;
163
+ border: 1px solid var(--gray-200);
164
+ border-radius: var(--radius-lg);
165
+ padding: var(--space-5);
166
+ text-align: center;
167
+ transition: all 0.2s ease;
168
+ box-shadow: var(--shadow-sm);
169
+ }
170
+
171
+ .status-card:hover {
172
+ transform: translateY(-2px);
173
+ box-shadow: var(--shadow-md);
174
+ border-color: var(--primary-purple);
175
+ }
176
+
177
+ .status-card:hover h4,
178
+ .status-card:hover .value,
179
+ .status-card:hover .subtitle {
180
+ color: var(--gray-800) !important;
181
+ }
182
+
183
+ .status-card:hover .value {
184
+ color: var(--primary-purple) !important;
185
+ }
186
+
187
+ .status-card h4 {
188
+ margin: 0 0 var(--space-2) 0;
189
+ font-size: var(--font-size-sm);
190
+ color: var(--gray-600);
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.05em;
193
+ font-weight: 600;
194
+ }
195
+
196
+ .status-card .value {
197
+ font-size: var(--font-size-2xl);
198
+ font-weight: 800;
199
+ color: var(--primary-purple);
200
+ margin: 0;
201
+ }
202
+
203
+ .status-card .subtitle {
204
+ font-size: var(--font-size-xs);
205
+ color: var(--gray-500);
206
+ margin: var(--space-1) 0 0 0;
207
+ }
208
+
209
+ /* Modern Buttons */
210
+ .modern-btn {
211
+ background: var(--primary-purple);
212
+ color: white;
213
+ border: none;
214
+ border-radius: var(--radius-md);
215
+ padding: var(--space-3) var(--space-6);
216
+ font-weight: 600;
217
+ font-size: var(--font-size-sm);
218
+ cursor: pointer;
219
+ transition: all 0.2s ease;
220
+ box-shadow: var(--shadow-sm);
221
+ position: relative;
222
+ }
223
+
224
+ .modern-btn:hover {
225
+ background: #5b21b6;
226
+ transform: translateY(-1px);
227
+ box-shadow: var(--shadow-md);
228
+ color: white !important;
229
+ }
230
+
231
+ .modern-btn:active {
232
+ transform: translateY(0);
233
+ }
234
+
235
+ /* Tooltip styles */
236
+ .tooltip {
237
+ position: relative;
238
+ display: inline-block;
239
+ }
240
+
241
+ .tooltip .tooltiptext {
242
+ visibility: hidden;
243
+ width: 200px;
244
+ background-color: var(--gray-800);
245
+ color: white;
246
+ text-align: center;
247
+ border-radius: var(--radius-md);
248
+ padding: var(--space-2) var(--space-3);
249
+ position: absolute;
250
+ z-index: 1;
251
+ bottom: 125%;
252
+ left: 50%;
253
+ margin-left: -100px;
254
+ opacity: 0;
255
+ transition: opacity 0.3s;
256
+ font-size: var(--font-size-xs);
257
+ box-shadow: var(--shadow-lg);
258
+ }
259
+
260
+ .tooltip .tooltiptext::after {
261
+ content: "";
262
+ position: absolute;
263
+ top: 100%;
264
+ left: 50%;
265
+ margin-left: -5px;
266
+ border-width: 5px;
267
+ border-style: solid;
268
+ border-color: var(--gray-800) transparent transparent transparent;
269
+ }
270
+
271
+ .tooltip:hover .tooltiptext {
272
+ visibility: visible;
273
+ opacity: 1;
274
+ }
275
+
276
+ .modern-btn-secondary {
277
+ background: var(--gray-700);
278
+ color: var(--gray-100);
279
+ }
280
+
281
+ .modern-btn-secondary:hover {
282
+ background: var(--gray-600);
283
+ }
284
+
285
+ .modern-btn-success {
286
+ background: var(--accent-green);
287
+ }
288
+
289
+ .modern-btn-success:hover {
290
+ background: #059669;
291
+ }
292
+
293
+ .modern-btn-danger {
294
+ background: var(--accent-red);
295
+ }
296
+
297
+ .modern-btn-danger:hover {
298
+ background: #dc2626;
299
+ }
300
+
301
+ /* Form Elements */
302
+ .modern-input {
303
+ background: white;
304
+ border: 1px solid var(--gray-300);
305
+ border-radius: var(--radius-md);
306
+ padding: var(--space-3);
307
+ color: var(--gray-800);
308
+ font-size: var(--font-size-sm);
309
+ transition: all 0.2s ease;
310
+ }
311
+
312
+ .modern-input:focus {
313
+ outline: none;
314
+ border-color: var(--primary-purple);
315
+ box-shadow: 0 0 0 3px rgb(107 70 193 / 0.1);
316
+ }
317
+
318
+ /* Hero Section - Laurier Style */
319
+ .hero-section {
320
+ background: var(--primary-purple);
321
+ border-radius: var(--radius-2xl);
322
+ padding: var(--space-10);
323
+ margin-bottom: var(--space-8);
324
+ text-align: left;
325
+ color: white;
326
+ box-shadow: var(--shadow-xl);
327
+ position: relative;
328
+ overflow: hidden;
329
+ }
330
+
331
+ .hero-section::before {
332
+ content: '';
333
+ position: absolute;
334
+ top: 0;
335
+ right: 0;
336
+ width: 40%;
337
+ height: 100%;
338
+ background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
339
+ border-radius: 0 var(--radius-2xl) var(--radius-2xl) 0;
340
+ }
341
+
342
+ .hero-section h1 {
343
+ color: white;
344
+ margin-bottom: var(--space-4);
345
+ }
346
+
347
+ .hero-section p {
348
+ font-size: var(--font-size-lg);
349
+ opacity: 0.9;
350
+ margin: 0;
351
+ }
352
+
353
+ /* Progress Indicators */
354
+ .progress-bar {
355
+ background: var(--gray-700);
356
+ border-radius: var(--radius-lg);
357
+ height: 8px;
358
+ overflow: hidden;
359
+ margin: var(--space-2) 0;
360
+ }
361
+
362
+ .progress-fill {
363
+ background: linear-gradient(90deg, var(--primary-purple), var(--primary-gold));
364
+ height: 100%;
365
+ transition: width 0.3s ease;
366
+ }
367
+
368
+ /* Badges */
369
+ .badge {
370
+ display: inline-block;
371
+ padding: var(--space-1) var(--space-2);
372
+ border-radius: var(--radius-sm);
373
+ font-size: var(--font-size-xs);
374
+ font-weight: 600;
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.05em;
377
+ }
378
+
379
+ .badge-success {
380
+ background: var(--accent-green);
381
+ color: white;
382
+ }
383
+
384
+ .badge-warning {
385
+ background: var(--accent-orange);
386
+ color: white;
387
+ }
388
+
389
+ .badge-danger {
390
+ background: var(--accent-red);
391
+ color: white;
392
+ }
393
+
394
+ .badge-info {
395
+ background: var(--accent-blue);
396
+ color: white;
397
+ }
398
+
399
+ /* Responsive Design */
400
+ @media (max-width: 768px) {
401
+ .main .block-container {
402
+ padding: var(--space-4) var(--space-2);
403
+ }
404
+
405
+ .status-grid {
406
+ grid-template-columns: 1fr;
407
+ }
408
+
409
+ .hero-section {
410
+ padding: var(--space-6);
411
+ }
412
+
413
+ h1 {
414
+ font-size: var(--font-size-2xl);
415
+ }
416
+ }
417
+
418
+ /* Loading States */
419
+ .loading {
420
+ display: inline-block;
421
+ width: 20px;
422
+ height: 20px;
423
+ border: 3px solid var(--gray-600);
424
+ border-radius: 50%;
425
+ border-top-color: var(--primary-purple);
426
+ animation: spin 1s ease-in-out infinite;
427
+ }
428
+
429
+ @keyframes spin {
430
+ to { transform: rotate(360deg); }
431
+ }
432
+
433
+ /* Animations */
434
+ .fade-in {
435
+ animation: fadeIn 0.5s ease-in;
436
+ }
437
+
438
+ @keyframes fadeIn {
439
+ from { opacity: 0; transform: translateY(20px); }
440
+ to { opacity: 1; transform: translateY(0); }
441
+ }
442
+
443
+ .slide-in {
444
+ animation: slideIn 0.3s ease-out;
445
+ }
446
+
447
+ @keyframes slideIn {
448
+ from { transform: translateX(-100%); }
449
+ to { transform: translateX(0); }
450
+ }
451
+
452
+ /* Accessibility */
453
+ .sr-only {
454
+ position: absolute;
455
+ width: 1px;
456
+ height: 1px;
457
+ padding: 0;
458
+ margin: -1px;
459
+ overflow: hidden;
460
+ clip: rect(0, 0, 0, 0);
461
+ white-space: nowrap;
462
+ border: 0;
463
+ }
464
+
465
+ /* Focus states for accessibility */
466
+ button:focus,
467
+ input:focus,
468
+ select:focus,
469
+ textarea:focus {
470
+ outline: 2px solid var(--primary-purple);
471
+ outline-offset: 2px;
472
+ }
473
+ </style>
474
+ """
475
+
476
+ @staticmethod
477
+ def create_hero_section(title: str, subtitle: str, user_email: str = None) -> str:
478
+ """Create a Laurier-style hero section"""
479
+ if user_email:
480
+ return f"""
481
+ <div class="hero-section fade-in">
482
+ <div style="position: relative; z-index: 2;">
483
+ <h1 style="font-size: 2.5rem; margin-bottom: var(--space-4); color: white;">💜💛 {title}</h1>
484
+ <p style="font-size: 1.25rem; margin-bottom: var(--space-6); color: white; opacity: 0.9;">{subtitle}</p>
485
+ <div style="margin-top: var(--space-6); padding: var(--space-5); background: rgba(255,255,255,0.15); border-radius: var(--radius-lg); backdrop-filter: blur(10px);">
486
+ <div style="font-size: var(--font-size-sm); opacity: 0.8; color: white;">Welcome,</div>
487
+ <div style="font-weight: 800; font-size: var(--font-size-xl); margin: var(--space-2) 0; color: white;">{user_email}</div>
488
+ <div style="font-size: var(--font-size-sm); opacity: 0.8; color: white;">Thank you for showing up for the community today. 💜💛</div>
489
+ </div>
490
+ </div>
491
+ </div>
492
+ """
493
+ else:
494
+ return f"""
495
+ <div class="hero-section fade-in">
496
+ <div style="position: relative; z-index: 2;">
497
+ <h1 style="font-size: 2.5rem; margin-bottom: var(--space-4); color: white;">💜💛 {title}</h1>
498
+ <p style="font-size: 1.25rem; color: white; opacity: 0.9;">{subtitle}</p>
499
+ </div>
500
+ </div>
501
+ """
502
+
503
+ @staticmethod
504
+ def create_status_cards(data: Dict[str, Any]) -> str:
505
+ """Create modern status cards"""
506
+ cards_html = '<div class="status-grid">'
507
+
508
+ for key, value in data.items():
509
+ if key == "shift_active":
510
+ cards_html += f"""
511
+ <div class="status-card slide-in">
512
+ <h4>Shift Active</h4>
513
+ <div class="value">{value}</div>
514
+ <div class="subtitle">since you signed in</div>
515
+ </div>
516
+ """
517
+ elif key == "items_today":
518
+ cards_html += f"""
519
+ <div class="status-card slide-in">
520
+ <h4>Items Logged Today</h4>
521
+ <div class="value">{value}</div>
522
+ <div class="subtitle">items processed</div>
523
+ </div>
524
+ """
525
+ elif key == "lifetime_hours":
526
+ cards_html += f"""
527
+ <div class="status-card slide-in">
528
+ <h4>Lifetime Hours</h4>
529
+ <div class="value">{value}</div>
530
+ <div class="subtitle">volunteer hours</div>
531
+ </div>
532
+ """
533
+
534
+ cards_html += '</div>'
535
+ return cards_html
536
+
537
+ @staticmethod
538
+ def create_modern_form_section(title: str, description: str = None) -> str:
539
+ """Create a modern form section header"""
540
+ desc_html = f'<p style="color: var(--gray-400); margin-bottom: var(--space-4);">{description}</p>' if description else ''
541
+ return f"""
542
+ <div class="modern-card fade-in">
543
+ <h3>{title}</h3>
544
+ {desc_html}
545
+ """
546
+
547
+ @staticmethod
548
+ def create_progress_indicator(current: int, total: int, label: str) -> str:
549
+ """Create a modern progress indicator"""
550
+ percentage = (current / total * 100) if total > 0 else 0
551
+ return f"""
552
+ <div style="margin: var(--space-4) 0;">
553
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-2);">
554
+ <span style="font-weight: 600; color: var(--gray-300);">{label}</span>
555
+ <span style="font-weight: 700; color: var(--primary-gold);">{current}/{total}</span>
556
+ </div>
557
+ <div class="progress-bar">
558
+ <div class="progress-fill" style="width: {percentage}%;"></div>
559
+ </div>
560
+ </div>
561
+ """
562
+
563
+ @staticmethod
564
+ def create_badge(text: str, variant: str = "info") -> str:
565
+ """Create a modern badge"""
566
+ return f'<span class="badge badge-{variant}">{text}</span>'
567
+
568
+ @staticmethod
569
+ def create_loading_spinner() -> str:
570
+ """Create a loading spinner"""
571
+ return '<div class="loading"></div>'
572
+
573
+ def apply_modern_ui():
574
+ """Apply modern UI styling to the Streamlit app"""
575
+ st.markdown(ModernUIComponents.get_modern_css(), unsafe_allow_html=True)
576
+
577
+ def create_modern_layout():
578
+ """Create a modern layout structure"""
579
+ return {
580
+ "container_style": "max-width: 1200px; margin: 0 auto;",
581
+ "sidebar_style": "background: var(--secondary-dark); border-right: 1px solid var(--gray-700);",
582
+ "main_style": "background: var(--primary-dark); padding: var(--space-6);"
583
+ }
backups/pre_test_backup_20250923_202554/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/pre_test_backup_20250923_202554/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/pre_test_backup_20250923_202554/streamlit_app.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Delightful Laurier-themed UX + enterprise data hygiene
3
+ # - OTP email login (Supabase)
4
+ # - Shift tracking & idle/8pm auto sign-out
5
+ # - Human-friendly visit_code (with DB trigger or safe fallback)
6
+ # - VLM-assisted item name
7
+ # - RPC ingest with validation/quarantine (fallback to direct insert)
8
+ # - Volunteer Impact card (today + lifetime)
9
+
10
+ from __future__ import annotations
11
+
12
+ import os, io, time, base64, re, uuid, json
13
+ from datetime import datetime, timedelta
14
+ from typing import Optional
15
+
16
+ import pytz
17
+ import requests
18
+ import pandas as pd
19
+ import streamlit as st
20
+ from PIL import Image, ImageOps, ImageEnhance
21
+ from supabase import create_client, Client
22
+
23
+ # ------------------------ App config ------------------------
24
+ os.environ.setdefault("HOME", "/tmp")
25
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
26
+
27
+ TZ = os.getenv("APP_TZ", "America/Toronto")
28
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
29
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
30
+
31
+ def local_now() -> datetime:
32
+ return datetime.now(pytz.timezone(TZ))
33
+
34
+ st.set_page_config(page_title="Care Count", layout="centered")
35
+ st.markdown("""
36
+ <style>
37
+ /* Laurier theme vibes */
38
+ :root { --cc-purple:#6d28d9; --cc-gold:#fde047; --cc-bg:#0b1420; --cc-panel:#0f1a2a; --cc-border:#1d2a44; }
39
+ .block-container { padding-top: 2rem; }
40
+ h1, h2, .stMarkdown h1, .stMarkdown h2 { letter-spacing:.2px }
41
+ .cc-pill { display:inline-block; padding:4px 10px; border-radius:999px; background:var(--cc-gold); color:#111827; font-weight:700; font-size:12px; }
42
+ .cc-hint { background:#10233b; border:1px solid #1f3b5b; color:#e6e8f0; padding:12px 16px; border-radius:10px; }
43
+ .cc-hero { background:#0f1a2a; border:1px solid #1f2a44; padding:16px 18px; border-radius:14px; }
44
+ .cc-btn-primary button { background:var(--cc-purple)!important; color:#fff!important; border:0!important; }
45
+ .cc-danger button { background:#7f1d1d!important; color:#fff!important; }
46
+ .status-card { display:flex; gap:16px; flex-wrap:wrap; }
47
+ .card { background:#0f1a2a; border:1px solid #1f2a44; border-radius:14px; padding:14px 16px; min-width:200px; }
48
+ .card h4 { margin:0 0 6px 0; font-size:0.95rem; color:#cbd5e1 }
49
+ .card .big { font-size:1.6rem; font-weight:700; color:#e5e7eb }
50
+ .small { color:#9aa3b2; font-size:12px }
51
+ </style>
52
+ """, unsafe_allow_html=True)
53
+
54
+ st.title("💜💛 Care Count")
55
+ st.caption("Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time.")
56
+
57
+ # ------------------------ Secrets & client ------------------------
58
+ # Robust env/secrets helpers that work with Streamlit (st.secrets) and .env/CI.
59
+ # Order of precedence: OS env > Streamlit secrets > default.
60
+
61
+ # Optional: allow plain `python` runs to pick up a local .env if present
62
+ try:
63
+ from dotenv import load_dotenv # pip install python-dotenv (optional)
64
+ load_dotenv()
65
+ except Exception:
66
+ pass
67
+
68
+ # Make Streamlit secrets safe to access even when not launched via `streamlit run`
69
+ try:
70
+ _SECRETS = getattr(st, "secrets", {})
71
+ except Exception:
72
+ _SECRETS = {}
73
+
74
+ def _is_useful(val: Optional[str]) -> bool:
75
+ return val is not None and str(val).strip() != ""
76
+
77
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
78
+ """
79
+ Flat TOML usage:
80
+ SUPABASE_URL = "..."
81
+ SUPABASE_KEY = "..."
82
+ Looks in OS env first, then st.secrets, else returns default.
83
+ """
84
+ # 1) OS env
85
+ v = os.getenv(name)
86
+ if _is_useful(v):
87
+ return v
88
+
89
+ # 2) Streamlit secrets (flat)
90
+ try:
91
+ if hasattr(_SECRETS, "get"):
92
+ v = _SECRETS.get(name, default)
93
+ if _is_useful(v):
94
+ return v
95
+ except Exception:
96
+ pass
97
+
98
+ # 3) default
99
+ return default
100
+
101
+ def require_secret(name: str) -> str:
102
+ """
103
+ Same as get_secret but fails fast with a clear error if missing/blank.
104
+ """
105
+ v = get_secret(name, None)
106
+ if not _is_useful(v):
107
+ st.error(
108
+ f"Missing secret: {name}. "
109
+ "Ensure it exists as an environment variable or in .streamlit/secrets.toml"
110
+ )
111
+ st.stop()
112
+ return str(v)
113
+
114
+ # (Optional) lightweight visibility check during setup. Comment out in prod.
115
+ # st.write("Secrets present:",
116
+ # bool(get_secret("SUPABASE_URL")),
117
+ # bool(get_secret("SUPABASE_KEY")))
118
+
119
+ # Required Supabase credentials (flat keys)
120
+ SUPABASE_URL = require_secret("SUPABASE_URL")
121
+ SUPABASE_KEY = require_secret("SUPABASE_KEY") # anon key (RLS should be ON)
122
+
123
+ # Initialize Supabase client
124
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
125
+
126
+ # Optional provider/config (all flat keys with safe defaults)
127
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
128
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
129
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
130
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
131
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
132
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
133
+
134
+
135
+
136
+ st.caption(f"Provider: `{PROVIDER}` · Model: `{GEMMA_MODEL}` · TZ: `{TZ}`")
137
+
138
+ # ------------------------ Light events/audit (non-blocking) ------------------------
139
+ def log_event(action: str, actor: Optional[str], details: dict):
140
+ try:
141
+ sb.table("events").insert({"actor_email": actor, "action": action, "details": details}).execute()
142
+ except Exception:
143
+ pass
144
+
145
+ # ------------------------ Auth (Email OTP) ------------------------
146
+ def auth_block() -> tuple[bool, Optional[str]]:
147
+ if "auth_email" not in st.session_state:
148
+ st.session_state["auth_email"] = None
149
+ if "user_email" in st.session_state:
150
+ return True, st.session_state["user_email"]
151
+
152
+ st.subheader("Sign in")
153
+ with st.form("otp_request", clear_on_submit=False, border=False):
154
+ email = st.text_input("Email", value=st.session_state.get("auth_email") or "", autocomplete="email")
155
+ send = st.form_submit_button("Send login code")
156
+ if send:
157
+ if not email or "@" not in email:
158
+ st.error("Please enter a valid email.")
159
+ else:
160
+ try:
161
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
162
+ st.session_state["auth_email"] = email
163
+ st.success("We emailed you a one-time code. Enter it to continue.")
164
+ except Exception as e:
165
+ st.error(f"Could not send code: {e}")
166
+
167
+ if st.session_state.get("auth_email"):
168
+ with st.form("otp_verify", clear_on_submit=True, border=False):
169
+ code = st.text_input("Enter 6-digit code", max_chars=6)
170
+ ok = st.form_submit_button("Verify & start shift")
171
+ if ok:
172
+ try:
173
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
174
+ if res and res.user:
175
+ email = st.session_state["auth_email"]
176
+ # Idempotent upsert for volunteer & start shift
177
+ sb.table("volunteers").upsert({
178
+ "email": email,
179
+ "last_login_at": datetime.utcnow().isoformat(),
180
+ "shift_started_at": datetime.utcnow().isoformat(),
181
+ "shift_ended_at": None
182
+ }, on_conflict="email").execute()
183
+ st.session_state["user_email"] = email
184
+ st.session_state["shift_started"] = True
185
+ st.session_state["last_activity_at"] = local_now()
186
+ log_event("login", email, {"method":"otp"})
187
+ st.toast("Welcome back! Shift started. 💜", icon="✅")
188
+ return True, email
189
+ except Exception as e:
190
+ st.error(f"Verification failed: {e}")
191
+ return False, None
192
+
193
+ def end_shift(email: str, reason: str):
194
+ try:
195
+ sb.table("volunteers").update({"shift_ended_at": datetime.utcnow().isoformat()}).eq("email", email).execute()
196
+ log_event("shift_end", email, {"reason": reason})
197
+ except Exception:
198
+ pass
199
+
200
+ def guard_cutoff_and_idle(email: str):
201
+ now = local_now()
202
+ last = st.session_state.get("last_activity_at")
203
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
204
+ end_shift(email, "inactivity")
205
+ st.session_state.clear()
206
+ st.info("You were logged out due to inactivity. Thank you for volunteering today!")
207
+ st.stop()
208
+ st.session_state["last_activity_at"] = now
209
+
210
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
211
+ if now >= cutoff:
212
+ end_shift(email, "cutoff_8pm")
213
+ st.session_state.clear()
214
+ st.info("We close the day at 8pm. Your shift has been ended. Thank you so much!")
215
+ st.stop()
216
+
217
+ signed_in, user_email = auth_block()
218
+ if not signed_in:
219
+ st.stop()
220
+ guard_cutoff_and_idle(user_email)
221
+
222
+ # Welcome banner (visceral, kind)
223
+ st.markdown(
224
+ f"""<div class="cc-hero">
225
+ <div class="small">Welcome,</div>
226
+ <div style="font-weight:800;font-size:1.15rem"> {user_email}</div>
227
+ <div class="small">Thank you for showing up for the community today. 💜💛</div>
228
+ </div>""",
229
+ unsafe_allow_html=True
230
+ )
231
+
232
+ # Sign-out button (prominent)
233
+ c_signout = st.columns([1,1,6])[1]
234
+ with c_signout:
235
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
236
+ if st.button("🔒 Sign out"):
237
+ end_shift(user_email, "manual")
238
+ st.session_state.clear()
239
+ st.success("Signed out. See you next time!")
240
+ st.stop()
241
+ st.markdown('</div>', unsafe_allow_html=True)
242
+
243
+ # ------------------------ Volunteer Impact card ------------------------
244
+ def fetch_volunteer_row(email: str) -> dict | None:
245
+ try:
246
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
247
+ except Exception:
248
+ return None
249
+
250
+ def items_today(email: str) -> int:
251
+ try:
252
+ day = local_now().strftime("%Y-%m-%d")
253
+ # try partitioned table first
254
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
255
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
256
+ .eq("volunteer", email).execute().data
257
+ return len(data or [])
258
+ except Exception:
259
+ try:
260
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
261
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
262
+ .eq("volunteer", email).execute().data
263
+ return len(data or [])
264
+ except Exception:
265
+ return 0
266
+
267
+ vrow = fetch_volunteer_row(user_email) or {}
268
+ shift_started_at = vrow.get("shift_started_at")
269
+ lifetime_hours = vrow.get("total_hours") # may not exist yet; handled below
270
+ mins_active = 0
271
+ try:
272
+ if shift_started_at:
273
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
274
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
275
+ except Exception:
276
+ pass
277
+
278
+ st.markdown('<div class="status-card">', unsafe_allow_html=True)
279
+ st.markdown(f'<div class="card"><h4>Shift active</h4><div class="big">{mins_active} min</div><div class="small">since you signed in</div></div>', unsafe_allow_html=True)
280
+ st.markdown(f'<div class="card"><h4>Items you logged today</h4><div class="big">{items_today(user_email)}</div></div>', unsafe_allow_html=True)
281
+ if isinstance(lifetime_hours, (int, float)):
282
+ st.markdown(f'<div class="card"><h4>Lifetime hours</h4><div class="big">{round(float(lifetime_hours),1)}</div></div>', unsafe_allow_html=True)
283
+ st.markdown('</div>', unsafe_allow_html=True)
284
+
285
+ # ------------------------ Image helpers ------------------------
286
+ def _to_png_bytes(img: Image.Image) -> bytes:
287
+ b = io.BytesIO(); img.save(b, format="PNG"); return b.getvalue()
288
+
289
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
290
+ img = img.convert("RGB")
291
+ w, h = img.size
292
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
293
+ if scale < 1.0: img = img.resize((int(w * scale), int(h * scale)))
294
+ img = ImageOps.autocontrast(img, cutoff=2)
295
+ img = ImageEnhance.Brightness(img).enhance(1.06)
296
+ img = ImageEnhance.Contrast(img).enhance(1.05)
297
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
298
+ return img
299
+
300
+ # ------------------------ VLM client ------------------------
301
+ SYSTEM_HINT = "You label item being held in the image for a food bank. Return ONLY the item name."
302
+
303
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
304
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
305
+ url = f"{base_url.rstrip('/')}/chat/completions"
306
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
307
+ payload = {
308
+ "model": model_id,
309
+ "temperature": 0,
310
+ "messages": [
311
+ {"role": "system", "content": SYSTEM_HINT},
312
+ {"role": "user", "content": [
313
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
314
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
315
+ ]}
316
+ ]
317
+ }
318
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
319
+ if r.status_code != 200:
320
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
321
+ data = r.json()
322
+ return (data["choices"][0]["message"]["content"] or "").strip()
323
+
324
+ def gemma_item_name(img_bytes: bytes) -> str:
325
+ if PROVIDER == "nebius":
326
+ if not NEBIUS_API_KEY: raise RuntimeError("NEBIUS_API_KEY missing")
327
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
328
+ elif PROVIDER == "featherless":
329
+ if not FEATH_API_KEY: raise RuntimeError("FEATHERLESS_API_KEY missing")
330
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
331
+ else:
332
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
333
+
334
+ # ------------------------ Normalization ------------------------
335
+ BRANDS = {
336
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
337
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
338
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
339
+ }
340
+ GENERIC_TYPES = {
341
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
342
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
343
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
344
+ "crackers","cookies","soup","insect killer","spray"
345
+ }
346
+ def normalize_item_name(s: str) -> str:
347
+ s = (s or "").strip()
348
+ if not s: return ""
349
+ low = re.sub(r"[®™]", "", s.lower())
350
+ for b in BRANDS: low = low.replace(b, "")
351
+ chosen = None
352
+ for t in GENERIC_TYPES:
353
+ if t in low: chosen = t; break
354
+ cleaned = " ".join(low.split())
355
+ return (chosen or cleaned.title())[:120]
356
+
357
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
358
+ if not v: return None
359
+ v = re.sub(r"\s+", " ", v).strip()
360
+ return v[:maxlen] if v else None
361
+
362
+ # ------------------------ Visit flow ------------------------
363
+ st.subheader("🪪 Anonymous Student Visit")
364
+
365
+ active_visit = st.session_state.get("active_visit")
366
+
367
+ def fallback_visit_code() -> str:
368
+ """Readable visit_code if DB trigger isn't present."""
369
+ try:
370
+ day = local_now().strftime("%Y-%m-%d")
371
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
372
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
373
+ seq = len(todays) + 1
374
+ except Exception:
375
+ seq = int(time.time()) % 1000
376
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
377
+
378
+ c1, c2, c3 = st.columns(3)
379
+ with c1:
380
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
381
+ if not active_visit and st.button("Start Visit"):
382
+ try:
383
+ payload = {
384
+ "visit_code": fallback_visit_code(), # DB trigger will override if present
385
+ "started_at": datetime.utcnow().isoformat(),
386
+ "ended_at": None,
387
+ "created_by": user_email
388
+ }
389
+ v = sb.table("visits").insert(payload).execute().data[0]
390
+ # If trigger generated code, keep that
391
+ if not v.get("visit_code"):
392
+ v["visit_code"] = payload["visit_code"]
393
+ st.session_state["active_visit"] = v
394
+ st.success(f"Visit #{v['id']} started · code: **{v['visit_code']}**")
395
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
396
+ except Exception as e:
397
+ st.error(f"Could not start visit: {e}")
398
+ st.markdown('</div>', unsafe_allow_html=True)
399
+
400
+ with c2:
401
+ if active_visit and st.button("End Visit (Checkout)"):
402
+ try:
403
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
404
+ .eq("id", active_visit["id"]).execute()
405
+ st.success("Visit checked out. Ready for the next student.")
406
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
407
+ st.session_state.pop("active_visit", None)
408
+ except Exception as e:
409
+ st.error(f"Could not end visit: {e}")
410
+
411
+ with c3:
412
+ if st.session_state.get("active_visit"):
413
+ v = st.session_state["active_visit"]
414
+ st.caption(f"Active visit_id: {v['id']} · code: {v.get('visit_code','')}")
415
+ else:
416
+ st.caption("No active visit.")
417
+
418
+ # ------------------------ Identify item ------------------------
419
+ st.subheader("📸 Identify item from image")
420
+
421
+ c4, c5 = st.columns(2)
422
+ with c4: cam = st.camera_input("Use your phone or webcam")
423
+ with c5: up = st.file_uploader("…or upload an image", type=["png","jpg","jpeg"])
424
+
425
+ img_file = cam or up
426
+ if img_file:
427
+ img = Image.open(img_file).convert("RGB")
428
+ st.image(img, use_container_width=True)
429
+ if st.button("🔍 Ask model for item name"):
430
+ t0, raw = time.time(), ""
431
+ try:
432
+ pre = preprocess_for_label(img); raw = gemma_item_name(_to_png_bytes(pre))
433
+ except Exception as e:
434
+ st.error(f"Provider error: {e}"); log_event("vlm_error", user_email, {"error": str(e)})
435
+ norm = normalize_item_name(raw)
436
+ if raw: st.success(f"🧠 Model: **{raw}**")
437
+ st.info(f"✨ Normalized: **{norm or '(unknown)'}** · ⏱️ {time.time()-t0:.2f}s")
438
+ st.session_state["scanned_item_name"] = norm or raw or ""
439
+ st.session_state["last_activity_at"] = local_now()
440
+
441
+ # ------------------------ Log item ------------------------
442
+ st.subheader("📬 Log item to current visit")
443
+
444
+ item_name = st.text_input("Item name", value=st.session_state.get("scanned_item_name",""))
445
+ quantity = st.number_input("Quantity", min_value=1, max_value=9999, step=1, value=1)
446
+ category = st.text_input("Category (optional)")
447
+ unit = st.text_input("Unit (optional, e.g., 500 mL, 1 L, 250 g)")
448
+ barcode = st.text_input("Barcode (optional)")
449
+
450
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
451
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
452
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
453
+
454
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
455
+ category: Optional[str], unit: Optional[str],
456
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
457
+ """
458
+ Call the RPC safe_ingest_visit_item if it exists.
459
+ Returns (ok, msg). If function is missing, raises to trigger fallback.
460
+ """
461
+ try:
462
+ res = sb.rpc("safe_ingest_visit_item", {
463
+ "p_email": email,
464
+ "p_visit_id": v_id,
465
+ "p_item_name": name,
466
+ "p_qty": qty,
467
+ "p_category": category,
468
+ "p_unit": unit,
469
+ "p_barcode": barcode,
470
+ "p_ts": ts_iso,
471
+ "p_ingest_id": ingest_id
472
+ }).execute()
473
+ # RPC returns a setof (ok boolean, msg text) — normalize
474
+ rows = res.data if isinstance(res.data, list) else []
475
+ if rows:
476
+ r0 = rows[0]
477
+ ok = bool(r0.get("ok", False))
478
+ msg = str(r0.get("msg", ""))
479
+ return ok, msg or ("ok" if ok else "failed")
480
+ # Some PostgREST setups return {} — treat as ok
481
+ return True, "ok"
482
+ except Exception as e:
483
+ # If function missing (42883) or not exposed, re-raise to use fallback
484
+ raise e
485
+
486
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
487
+ category: Optional[str], unit: Optional[str],
488
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
489
+ """Write into visit_items_p if present, else visit_items (keeps app working)."""
490
+ payload = {
491
+ "visit_id": v_id,
492
+ "timestamp": ts_iso,
493
+ "volunteer": email,
494
+ "item_name": name,
495
+ "category": category,
496
+ "unit": unit,
497
+ "qty": qty,
498
+ "barcode": barcode,
499
+ "weather_type": None,
500
+ "temp_c": None,
501
+ "ingest_id": ingest_id
502
+ }
503
+ try:
504
+ sb.table("visit_items_p").insert(payload).execute()
505
+ except Exception:
506
+ # legacy table fallback
507
+ payload.pop("ingest_id", None)
508
+ sb.table("visit_items").insert(payload).execute()
509
+
510
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
511
+ save_disabled = not st.session_state.get("active_visit")
512
+ if st.button("✅ Save Item to Visit", disabled=save_disabled):
513
+ v = st.session_state.get("active_visit")
514
+ if not v:
515
+ st.warning("Start a visit first, then save items.")
516
+ else:
517
+ name_clean = clean_text(item_name, 120)
518
+ if not name_clean:
519
+ st.warning("Item name is required.")
520
+ else:
521
+ ts_iso = datetime.utcnow().isoformat()
522
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
523
+ try:
524
+ ok, msg = try_rpc_ingest(
525
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
526
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
527
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
528
+ )
529
+ if ok:
530
+ st.success("Item logged ✅")
531
+ else:
532
+ st.warning(f"Ingest said: {msg}. (Will try direct insert.)")
533
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
534
+ clean_text(category,80), clean_text(unit,40),
535
+ clean_text(barcode,64), ts_iso, ingest_id)
536
+ st.success("Item logged ✅ (fallback)")
537
+ except Exception as e:
538
+ # RPC missing or failed → direct insert
539
+ try:
540
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
541
+ clean_text(category,80), clean_text(unit,40),
542
+ clean_text(barcode,64), ts_iso, ingest_id)
543
+ st.success("Item logged ✅ (fallback)")
544
+ except Exception as e2:
545
+ st.error(f"Ingest failed: {e2}")
546
+ st.session_state["last_activity_at"] = local_now()
547
+ st.session_state["scanned_item_name"] = name_clean
548
+ st.markdown('</div>', unsafe_allow_html=True)
549
+
550
+ # ------------------------ Visit items view + delete ------------------------
551
+ def load_items_for_visit(visit_id: int) -> list[dict]:
552
+ # Prefer partitioned table
553
+ try:
554
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
555
+ .order("timestamp", desc=True).limit(500).execute().data or []
556
+ except Exception:
557
+ try:
558
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
559
+ .order("timestamp", desc=True).limit(500).execute().data or []
560
+ except Exception:
561
+ return []
562
+
563
+ def delete_item(table: str, item_id: int):
564
+ try:
565
+ sb.table(table).delete().eq("id", item_id).execute()
566
+ except Exception as e:
567
+ raise e
568
+
569
+ if st.session_state.get("active_visit"):
570
+ st.subheader("🧾 Items in this visit")
571
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
572
+ if rows:
573
+ df = pd.DataFrame(rows)
574
+ st.dataframe(df, use_container_width=True)
575
+ with st.expander("🗑️ Delete an item (if mis-logged)"):
576
+ ids = [r["id"] for r in rows if "id" in r]
577
+ choice = st.selectbox("Choose item id", ids) if ids else None
578
+ st.markdown('<div class="cc-danger">', unsafe_allow_html=True)
579
+ if st.button("Delete selected", disabled=not bool(ids)):
580
+ if choice is None:
581
+ st.warning("Pick an id.")
582
+ else:
583
+ # try both tables safely
584
+ try:
585
+ delete_item("visit_items_p", int(choice))
586
+ except Exception:
587
+ delete_item("visit_items", int(choice))
588
+ st.success("Deleted.")
589
+ st.markdown('</div>', unsafe_allow_html=True)
590
+ else:
591
+ st.caption("No items logged for this visit yet.")
592
+
593
+ # ------------------------ Analytics (light) ------------------------
594
+ st.subheader("📈 Today")
595
+ try:
596
+ daily = sb.table("v_daily_activity").select("*").execute().data
597
+ today_str = local_now().strftime("%Y-%m-%d")
598
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
599
+ visits = int(today["visits"]) if today and "visits" in today else 0
600
+ items = int(today["items"]) if today and "items" in today else 0
601
+ st.markdown(f"**Visits:** {visits} · **Items:** {items}")
602
+ except Exception:
603
+ st.caption("Analytics view unavailable yet.")
604
+
605
+ # ------------------------ Gentle reminder ------------------------
606
+ st.markdown(
607
+ """<div class="cc-hint">
608
+ 💡 When you’re done, please <b>End Visit</b> and <b>Sign out</b>.<br>
609
+ We’ll auto-sign you out after inactivity or at 8pm local time. 💜
610
+ </div>""",
611
+ unsafe_allow_html=True
612
+ )
backups/pre_test_backup_20250923_202613/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/pre_test_backup_20250923_202613/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/pre_test_backup_20250923_202613/streamlit_app.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Delightful Laurier-themed UX + enterprise data hygiene
3
+ # - OTP email login (Supabase)
4
+ # - Shift tracking & idle/8pm auto sign-out
5
+ # - Human-friendly visit_code (with DB trigger or safe fallback)
6
+ # - VLM-assisted item name
7
+ # - RPC ingest with validation/quarantine (fallback to direct insert)
8
+ # - Volunteer Impact card (today + lifetime)
9
+
10
+ from __future__ import annotations
11
+
12
+ import os, io, time, base64, re, uuid, json
13
+ from datetime import datetime, timedelta
14
+ from typing import Optional
15
+
16
+ import pytz
17
+ import requests
18
+ import pandas as pd
19
+ import streamlit as st
20
+ from PIL import Image, ImageOps, ImageEnhance
21
+ from supabase import create_client, Client
22
+
23
+ # ------------------------ App config ------------------------
24
+ os.environ.setdefault("HOME", "/tmp")
25
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
26
+
27
+ TZ = os.getenv("APP_TZ", "America/Toronto")
28
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
29
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
30
+
31
+ def local_now() -> datetime:
32
+ return datetime.now(pytz.timezone(TZ))
33
+
34
+ st.set_page_config(page_title="Care Count", layout="centered")
35
+ st.markdown("""
36
+ <style>
37
+ /* Laurier theme vibes */
38
+ :root { --cc-purple:#6d28d9; --cc-gold:#fde047; --cc-bg:#0b1420; --cc-panel:#0f1a2a; --cc-border:#1d2a44; }
39
+ .block-container { padding-top: 2rem; }
40
+ h1, h2, .stMarkdown h1, .stMarkdown h2 { letter-spacing:.2px }
41
+ .cc-pill { display:inline-block; padding:4px 10px; border-radius:999px; background:var(--cc-gold); color:#111827; font-weight:700; font-size:12px; }
42
+ .cc-hint { background:#10233b; border:1px solid #1f3b5b; color:#e6e8f0; padding:12px 16px; border-radius:10px; }
43
+ .cc-hero { background:#0f1a2a; border:1px solid #1f2a44; padding:16px 18px; border-radius:14px; }
44
+ .cc-btn-primary button { background:var(--cc-purple)!important; color:#fff!important; border:0!important; }
45
+ .cc-danger button { background:#7f1d1d!important; color:#fff!important; }
46
+ .status-card { display:flex; gap:16px; flex-wrap:wrap; }
47
+ .card { background:#0f1a2a; border:1px solid #1f2a44; border-radius:14px; padding:14px 16px; min-width:200px; }
48
+ .card h4 { margin:0 0 6px 0; font-size:0.95rem; color:#cbd5e1 }
49
+ .card .big { font-size:1.6rem; font-weight:700; color:#e5e7eb }
50
+ .small { color:#9aa3b2; font-size:12px }
51
+ </style>
52
+ """, unsafe_allow_html=True)
53
+
54
+ st.title("💜💛 Care Count")
55
+ st.caption("Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time.")
56
+
57
+ # ------------------------ Secrets & client ------------------------
58
+ # Robust env/secrets helpers that work with Streamlit (st.secrets) and .env/CI.
59
+ # Order of precedence: OS env > Streamlit secrets > default.
60
+
61
+ # Optional: allow plain `python` runs to pick up a local .env if present
62
+ try:
63
+ from dotenv import load_dotenv # pip install python-dotenv (optional)
64
+ load_dotenv()
65
+ except Exception:
66
+ pass
67
+
68
+ # Make Streamlit secrets safe to access even when not launched via `streamlit run`
69
+ try:
70
+ _SECRETS = getattr(st, "secrets", {})
71
+ except Exception:
72
+ _SECRETS = {}
73
+
74
+ def _is_useful(val: Optional[str]) -> bool:
75
+ return val is not None and str(val).strip() != ""
76
+
77
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
78
+ """
79
+ Flat TOML usage:
80
+ SUPABASE_URL = "..."
81
+ SUPABASE_KEY = "..."
82
+ Looks in OS env first, then st.secrets, else returns default.
83
+ """
84
+ # 1) OS env
85
+ v = os.getenv(name)
86
+ if _is_useful(v):
87
+ return v
88
+
89
+ # 2) Streamlit secrets (flat)
90
+ try:
91
+ if hasattr(_SECRETS, "get"):
92
+ v = _SECRETS.get(name, default)
93
+ if _is_useful(v):
94
+ return v
95
+ except Exception:
96
+ pass
97
+
98
+ # 3) default
99
+ return default
100
+
101
+ def require_secret(name: str) -> str:
102
+ """
103
+ Same as get_secret but fails fast with a clear error if missing/blank.
104
+ """
105
+ v = get_secret(name, None)
106
+ if not _is_useful(v):
107
+ st.error(
108
+ f"Missing secret: {name}. "
109
+ "Ensure it exists as an environment variable or in .streamlit/secrets.toml"
110
+ )
111
+ st.stop()
112
+ return str(v)
113
+
114
+ # (Optional) lightweight visibility check during setup. Comment out in prod.
115
+ # st.write("Secrets present:",
116
+ # bool(get_secret("SUPABASE_URL")),
117
+ # bool(get_secret("SUPABASE_KEY")))
118
+
119
+ # Required Supabase credentials (flat keys)
120
+ SUPABASE_URL = require_secret("SUPABASE_URL")
121
+ SUPABASE_KEY = require_secret("SUPABASE_KEY") # anon key (RLS should be ON)
122
+
123
+ # Initialize Supabase client
124
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
125
+
126
+ # Optional provider/config (all flat keys with safe defaults)
127
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
128
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
129
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
130
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
131
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
132
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
133
+
134
+
135
+
136
+ st.caption(f"Provider: `{PROVIDER}` · Model: `{GEMMA_MODEL}` · TZ: `{TZ}`")
137
+
138
+ # ------------------------ Light events/audit (non-blocking) ------------------------
139
+ def log_event(action: str, actor: Optional[str], details: dict):
140
+ try:
141
+ sb.table("events").insert({"actor_email": actor, "action": action, "details": details}).execute()
142
+ except Exception:
143
+ pass
144
+
145
+ # ------------------------ Auth (Email OTP) ------------------------
146
+ def auth_block() -> tuple[bool, Optional[str]]:
147
+ if "auth_email" not in st.session_state:
148
+ st.session_state["auth_email"] = None
149
+ if "user_email" in st.session_state:
150
+ return True, st.session_state["user_email"]
151
+
152
+ st.subheader("Sign in")
153
+ with st.form("otp_request", clear_on_submit=False, border=False):
154
+ email = st.text_input("Email", value=st.session_state.get("auth_email") or "", autocomplete="email")
155
+ send = st.form_submit_button("Send login code")
156
+ if send:
157
+ if not email or "@" not in email:
158
+ st.error("Please enter a valid email.")
159
+ else:
160
+ try:
161
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
162
+ st.session_state["auth_email"] = email
163
+ st.success("We emailed you a one-time code. Enter it to continue.")
164
+ except Exception as e:
165
+ st.error(f"Could not send code: {e}")
166
+
167
+ if st.session_state.get("auth_email"):
168
+ with st.form("otp_verify", clear_on_submit=True, border=False):
169
+ code = st.text_input("Enter 6-digit code", max_chars=6)
170
+ ok = st.form_submit_button("Verify & start shift")
171
+ if ok:
172
+ try:
173
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
174
+ if res and res.user:
175
+ email = st.session_state["auth_email"]
176
+ # Idempotent upsert for volunteer & start shift
177
+ sb.table("volunteers").upsert({
178
+ "email": email,
179
+ "last_login_at": datetime.utcnow().isoformat(),
180
+ "shift_started_at": datetime.utcnow().isoformat(),
181
+ "shift_ended_at": None
182
+ }, on_conflict="email").execute()
183
+ st.session_state["user_email"] = email
184
+ st.session_state["shift_started"] = True
185
+ st.session_state["last_activity_at"] = local_now()
186
+ log_event("login", email, {"method":"otp"})
187
+ st.toast("Welcome back! Shift started. 💜", icon="✅")
188
+ return True, email
189
+ except Exception as e:
190
+ st.error(f"Verification failed: {e}")
191
+ return False, None
192
+
193
+ def end_shift(email: str, reason: str):
194
+ try:
195
+ sb.table("volunteers").update({"shift_ended_at": datetime.utcnow().isoformat()}).eq("email", email).execute()
196
+ log_event("shift_end", email, {"reason": reason})
197
+ except Exception:
198
+ pass
199
+
200
+ def guard_cutoff_and_idle(email: str):
201
+ now = local_now()
202
+ last = st.session_state.get("last_activity_at")
203
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
204
+ end_shift(email, "inactivity")
205
+ st.session_state.clear()
206
+ st.info("You were logged out due to inactivity. Thank you for volunteering today!")
207
+ st.stop()
208
+ st.session_state["last_activity_at"] = now
209
+
210
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
211
+ if now >= cutoff:
212
+ end_shift(email, "cutoff_8pm")
213
+ st.session_state.clear()
214
+ st.info("We close the day at 8pm. Your shift has been ended. Thank you so much!")
215
+ st.stop()
216
+
217
+ signed_in, user_email = auth_block()
218
+ if not signed_in:
219
+ st.stop()
220
+ guard_cutoff_and_idle(user_email)
221
+
222
+ # Welcome banner (visceral, kind)
223
+ st.markdown(
224
+ f"""<div class="cc-hero">
225
+ <div class="small">Welcome,</div>
226
+ <div style="font-weight:800;font-size:1.15rem"> {user_email}</div>
227
+ <div class="small">Thank you for showing up for the community today. 💜💛</div>
228
+ </div>""",
229
+ unsafe_allow_html=True
230
+ )
231
+
232
+ # Sign-out button (prominent)
233
+ c_signout = st.columns([1,1,6])[1]
234
+ with c_signout:
235
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
236
+ if st.button("🔒 Sign out"):
237
+ end_shift(user_email, "manual")
238
+ st.session_state.clear()
239
+ st.success("Signed out. See you next time!")
240
+ st.stop()
241
+ st.markdown('</div>', unsafe_allow_html=True)
242
+
243
+ # ------------------------ Volunteer Impact card ------------------------
244
+ def fetch_volunteer_row(email: str) -> dict | None:
245
+ try:
246
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
247
+ except Exception:
248
+ return None
249
+
250
+ def items_today(email: str) -> int:
251
+ try:
252
+ day = local_now().strftime("%Y-%m-%d")
253
+ # try partitioned table first
254
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
255
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
256
+ .eq("volunteer", email).execute().data
257
+ return len(data or [])
258
+ except Exception:
259
+ try:
260
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
261
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
262
+ .eq("volunteer", email).execute().data
263
+ return len(data or [])
264
+ except Exception:
265
+ return 0
266
+
267
+ vrow = fetch_volunteer_row(user_email) or {}
268
+ shift_started_at = vrow.get("shift_started_at")
269
+ lifetime_hours = vrow.get("total_hours") # may not exist yet; handled below
270
+ mins_active = 0
271
+ try:
272
+ if shift_started_at:
273
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
274
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
275
+ except Exception:
276
+ pass
277
+
278
+ st.markdown('<div class="status-card">', unsafe_allow_html=True)
279
+ st.markdown(f'<div class="card"><h4>Shift active</h4><div class="big">{mins_active} min</div><div class="small">since you signed in</div></div>', unsafe_allow_html=True)
280
+ st.markdown(f'<div class="card"><h4>Items you logged today</h4><div class="big">{items_today(user_email)}</div></div>', unsafe_allow_html=True)
281
+ if isinstance(lifetime_hours, (int, float)):
282
+ st.markdown(f'<div class="card"><h4>Lifetime hours</h4><div class="big">{round(float(lifetime_hours),1)}</div></div>', unsafe_allow_html=True)
283
+ st.markdown('</div>', unsafe_allow_html=True)
284
+
285
+ # ------------------------ Image helpers ------------------------
286
+ def _to_png_bytes(img: Image.Image) -> bytes:
287
+ b = io.BytesIO(); img.save(b, format="PNG"); return b.getvalue()
288
+
289
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
290
+ img = img.convert("RGB")
291
+ w, h = img.size
292
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
293
+ if scale < 1.0: img = img.resize((int(w * scale), int(h * scale)))
294
+ img = ImageOps.autocontrast(img, cutoff=2)
295
+ img = ImageEnhance.Brightness(img).enhance(1.06)
296
+ img = ImageEnhance.Contrast(img).enhance(1.05)
297
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
298
+ return img
299
+
300
+ # ------------------------ VLM client ------------------------
301
+ SYSTEM_HINT = "You label item being held in the image for a food bank. Return ONLY the item name."
302
+
303
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
304
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
305
+ url = f"{base_url.rstrip('/')}/chat/completions"
306
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
307
+ payload = {
308
+ "model": model_id,
309
+ "temperature": 0,
310
+ "messages": [
311
+ {"role": "system", "content": SYSTEM_HINT},
312
+ {"role": "user", "content": [
313
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
314
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
315
+ ]}
316
+ ]
317
+ }
318
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
319
+ if r.status_code != 200:
320
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
321
+ data = r.json()
322
+ return (data["choices"][0]["message"]["content"] or "").strip()
323
+
324
+ def gemma_item_name(img_bytes: bytes) -> str:
325
+ if PROVIDER == "nebius":
326
+ if not NEBIUS_API_KEY: raise RuntimeError("NEBIUS_API_KEY missing")
327
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
328
+ elif PROVIDER == "featherless":
329
+ if not FEATH_API_KEY: raise RuntimeError("FEATHERLESS_API_KEY missing")
330
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
331
+ else:
332
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
333
+
334
+ # ------------------------ Normalization ------------------------
335
+ BRANDS = {
336
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
337
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
338
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
339
+ }
340
+ GENERIC_TYPES = {
341
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
342
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
343
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
344
+ "crackers","cookies","soup","insect killer","spray"
345
+ }
346
+ def normalize_item_name(s: str) -> str:
347
+ s = (s or "").strip()
348
+ if not s: return ""
349
+ low = re.sub(r"[®™]", "", s.lower())
350
+ for b in BRANDS: low = low.replace(b, "")
351
+ chosen = None
352
+ for t in GENERIC_TYPES:
353
+ if t in low: chosen = t; break
354
+ cleaned = " ".join(low.split())
355
+ return (chosen or cleaned.title())[:120]
356
+
357
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
358
+ if not v: return None
359
+ v = re.sub(r"\s+", " ", v).strip()
360
+ return v[:maxlen] if v else None
361
+
362
+ # ------------------------ Visit flow ------------------------
363
+ st.subheader("🪪 Anonymous Student Visit")
364
+
365
+ active_visit = st.session_state.get("active_visit")
366
+
367
+ def fallback_visit_code() -> str:
368
+ """Readable visit_code if DB trigger isn't present."""
369
+ try:
370
+ day = local_now().strftime("%Y-%m-%d")
371
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
372
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
373
+ seq = len(todays) + 1
374
+ except Exception:
375
+ seq = int(time.time()) % 1000
376
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
377
+
378
+ c1, c2, c3 = st.columns(3)
379
+ with c1:
380
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
381
+ if not active_visit and st.button("Start Visit"):
382
+ try:
383
+ payload = {
384
+ "visit_code": fallback_visit_code(), # DB trigger will override if present
385
+ "started_at": datetime.utcnow().isoformat(),
386
+ "ended_at": None,
387
+ "created_by": user_email
388
+ }
389
+ v = sb.table("visits").insert(payload).execute().data[0]
390
+ # If trigger generated code, keep that
391
+ if not v.get("visit_code"):
392
+ v["visit_code"] = payload["visit_code"]
393
+ st.session_state["active_visit"] = v
394
+ st.success(f"Visit #{v['id']} started · code: **{v['visit_code']}**")
395
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
396
+ except Exception as e:
397
+ st.error(f"Could not start visit: {e}")
398
+ st.markdown('</div>', unsafe_allow_html=True)
399
+
400
+ with c2:
401
+ if active_visit and st.button("End Visit (Checkout)"):
402
+ try:
403
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
404
+ .eq("id", active_visit["id"]).execute()
405
+ st.success("Visit checked out. Ready for the next student.")
406
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
407
+ st.session_state.pop("active_visit", None)
408
+ except Exception as e:
409
+ st.error(f"Could not end visit: {e}")
410
+
411
+ with c3:
412
+ if st.session_state.get("active_visit"):
413
+ v = st.session_state["active_visit"]
414
+ st.caption(f"Active visit_id: {v['id']} · code: {v.get('visit_code','')}")
415
+ else:
416
+ st.caption("No active visit.")
417
+
418
+ # ------------------------ Identify item ------------------------
419
+ st.subheader("📸 Identify item from image")
420
+
421
+ c4, c5 = st.columns(2)
422
+ with c4: cam = st.camera_input("Use your phone or webcam")
423
+ with c5: up = st.file_uploader("…or upload an image", type=["png","jpg","jpeg"])
424
+
425
+ img_file = cam or up
426
+ if img_file:
427
+ img = Image.open(img_file).convert("RGB")
428
+ st.image(img, use_container_width=True)
429
+ if st.button("🔍 Ask model for item name"):
430
+ t0, raw = time.time(), ""
431
+ try:
432
+ pre = preprocess_for_label(img); raw = gemma_item_name(_to_png_bytes(pre))
433
+ except Exception as e:
434
+ st.error(f"Provider error: {e}"); log_event("vlm_error", user_email, {"error": str(e)})
435
+ norm = normalize_item_name(raw)
436
+ if raw: st.success(f"🧠 Model: **{raw}**")
437
+ st.info(f"✨ Normalized: **{norm or '(unknown)'}** · ⏱️ {time.time()-t0:.2f}s")
438
+ st.session_state["scanned_item_name"] = norm or raw or ""
439
+ st.session_state["last_activity_at"] = local_now()
440
+
441
+ # ------------------------ Log item ------------------------
442
+ st.subheader("📬 Log item to current visit")
443
+
444
+ item_name = st.text_input("Item name", value=st.session_state.get("scanned_item_name",""))
445
+ quantity = st.number_input("Quantity", min_value=1, max_value=9999, step=1, value=1)
446
+ category = st.text_input("Category (optional)")
447
+ unit = st.text_input("Unit (optional, e.g., 500 mL, 1 L, 250 g)")
448
+ barcode = st.text_input("Barcode (optional)")
449
+
450
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
451
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
452
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
453
+
454
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
455
+ category: Optional[str], unit: Optional[str],
456
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
457
+ """
458
+ Call the RPC safe_ingest_visit_item if it exists.
459
+ Returns (ok, msg). If function is missing, raises to trigger fallback.
460
+ """
461
+ try:
462
+ res = sb.rpc("safe_ingest_visit_item", {
463
+ "p_email": email,
464
+ "p_visit_id": v_id,
465
+ "p_item_name": name,
466
+ "p_qty": qty,
467
+ "p_category": category,
468
+ "p_unit": unit,
469
+ "p_barcode": barcode,
470
+ "p_ts": ts_iso,
471
+ "p_ingest_id": ingest_id
472
+ }).execute()
473
+ # RPC returns a setof (ok boolean, msg text) — normalize
474
+ rows = res.data if isinstance(res.data, list) else []
475
+ if rows:
476
+ r0 = rows[0]
477
+ ok = bool(r0.get("ok", False))
478
+ msg = str(r0.get("msg", ""))
479
+ return ok, msg or ("ok" if ok else "failed")
480
+ # Some PostgREST setups return {} — treat as ok
481
+ return True, "ok"
482
+ except Exception as e:
483
+ # If function missing (42883) or not exposed, re-raise to use fallback
484
+ raise e
485
+
486
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
487
+ category: Optional[str], unit: Optional[str],
488
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
489
+ """Write into visit_items_p if present, else visit_items (keeps app working)."""
490
+ payload = {
491
+ "visit_id": v_id,
492
+ "timestamp": ts_iso,
493
+ "volunteer": email,
494
+ "item_name": name,
495
+ "category": category,
496
+ "unit": unit,
497
+ "qty": qty,
498
+ "barcode": barcode,
499
+ "weather_type": None,
500
+ "temp_c": None,
501
+ "ingest_id": ingest_id
502
+ }
503
+ try:
504
+ sb.table("visit_items_p").insert(payload).execute()
505
+ except Exception:
506
+ # legacy table fallback
507
+ payload.pop("ingest_id", None)
508
+ sb.table("visit_items").insert(payload).execute()
509
+
510
+ st.markdown('<div class="cc-btn-primary">', unsafe_allow_html=True)
511
+ save_disabled = not st.session_state.get("active_visit")
512
+ if st.button("✅ Save Item to Visit", disabled=save_disabled):
513
+ v = st.session_state.get("active_visit")
514
+ if not v:
515
+ st.warning("Start a visit first, then save items.")
516
+ else:
517
+ name_clean = clean_text(item_name, 120)
518
+ if not name_clean:
519
+ st.warning("Item name is required.")
520
+ else:
521
+ ts_iso = datetime.utcnow().isoformat()
522
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
523
+ try:
524
+ ok, msg = try_rpc_ingest(
525
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
526
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
527
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
528
+ )
529
+ if ok:
530
+ st.success("Item logged ✅")
531
+ else:
532
+ st.warning(f"Ingest said: {msg}. (Will try direct insert.)")
533
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
534
+ clean_text(category,80), clean_text(unit,40),
535
+ clean_text(barcode,64), ts_iso, ingest_id)
536
+ st.success("Item logged ✅ (fallback)")
537
+ except Exception as e:
538
+ # RPC missing or failed → direct insert
539
+ try:
540
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
541
+ clean_text(category,80), clean_text(unit,40),
542
+ clean_text(barcode,64), ts_iso, ingest_id)
543
+ st.success("Item logged ✅ (fallback)")
544
+ except Exception as e2:
545
+ st.error(f"Ingest failed: {e2}")
546
+ st.session_state["last_activity_at"] = local_now()
547
+ st.session_state["scanned_item_name"] = name_clean
548
+ st.markdown('</div>', unsafe_allow_html=True)
549
+
550
+ # ------------------------ Visit items view + delete ------------------------
551
+ def load_items_for_visit(visit_id: int) -> list[dict]:
552
+ # Prefer partitioned table
553
+ try:
554
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
555
+ .order("timestamp", desc=True).limit(500).execute().data or []
556
+ except Exception:
557
+ try:
558
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
559
+ .order("timestamp", desc=True).limit(500).execute().data or []
560
+ except Exception:
561
+ return []
562
+
563
+ def delete_item(table: str, item_id: int):
564
+ try:
565
+ sb.table(table).delete().eq("id", item_id).execute()
566
+ except Exception as e:
567
+ raise e
568
+
569
+ if st.session_state.get("active_visit"):
570
+ st.subheader("🧾 Items in this visit")
571
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
572
+ if rows:
573
+ df = pd.DataFrame(rows)
574
+ st.dataframe(df, use_container_width=True)
575
+ with st.expander("🗑️ Delete an item (if mis-logged)"):
576
+ ids = [r["id"] for r in rows if "id" in r]
577
+ choice = st.selectbox("Choose item id", ids) if ids else None
578
+ st.markdown('<div class="cc-danger">', unsafe_allow_html=True)
579
+ if st.button("Delete selected", disabled=not bool(ids)):
580
+ if choice is None:
581
+ st.warning("Pick an id.")
582
+ else:
583
+ # try both tables safely
584
+ try:
585
+ delete_item("visit_items_p", int(choice))
586
+ except Exception:
587
+ delete_item("visit_items", int(choice))
588
+ st.success("Deleted.")
589
+ st.markdown('</div>', unsafe_allow_html=True)
590
+ else:
591
+ st.caption("No items logged for this visit yet.")
592
+
593
+ # ------------------------ Analytics (light) ------------------------
594
+ st.subheader("📈 Today")
595
+ try:
596
+ daily = sb.table("v_daily_activity").select("*").execute().data
597
+ today_str = local_now().strftime("%Y-%m-%d")
598
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
599
+ visits = int(today["visits"]) if today and "visits" in today else 0
600
+ items = int(today["items"]) if today and "items" in today else 0
601
+ st.markdown(f"**Visits:** {visits} · **Items:** {items}")
602
+ except Exception:
603
+ st.caption("Analytics view unavailable yet.")
604
+
605
+ # ------------------------ Gentle reminder ------------------------
606
+ st.markdown(
607
+ """<div class="cc-hint">
608
+ 💡 When you’re done, please <b>End Visit</b> and <b>Sign out</b>.<br>
609
+ We’ll auto-sign you out after inactivity or at 8pm local time. 💜
610
+ </div>""",
611
+ unsafe_allow_html=True
612
+ )
backups/pre_test_backup_20250923_202627/requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ streamlit>=1.33
2
+ pandas>=2.1
3
+ Pillow>=10.1
4
+ supabase>=2.5
5
+ huggingface_hub>=0.24.6
6
+ requests>=2.31
7
+ python-dotenv>=1.0.0
8
+ pytz>=2023.3
9
+
backups/pre_test_backup_20250923_202627/secrets.toml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase connection
2
+ SUPABASE_URL = "https://oudjcqhyldpncjbxohsg.supabase.co"
3
+ SUPABASE_KEY ="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im91ZGpjcWh5bGRwbmNqYnhvaHNnIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzMDgyNDQsImV4cCI6MjA3MDg4NDI0NH0.SYwTtCe3r7WOZ25CRggL-lBkvGPjKnQhY_dT4Kjv9Dg"
4
+
5
+ # App settings
6
+ APP_TZ = "America/Toronto"
7
+
8
+ # Hugging Face
9
+ HF_TOKEN = "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"
10
+
11
+ # Optional VLM model provider
12
+ PROVIDER = "nebius"
13
+ GEMMA_MODEL = "google/gemma-3-27b-it"
14
+ NEBIUS_API_KEY = "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwMjk4MzMxNjQ1OTQzNjA5ODYxMyIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkxNTEzMTE3MiwidXVpZCI6IjJmOTUxZmM1LWEyNTgtNDg5OC1iNWI4LTkwZTljY2RkZjU2NSIsIm5hbWUiOiJjYXJlIGNvdW50IGFwcCIsImV4cGlyZXNfYXQiOiIyMDMwLTA5LTA4VDIwOjUyOjUyKzAwMDAifQ.awAHOieD93pCnYmFMgRQ64-i6yZdO0XvXczD4eS6xEI"
15
+ NEBIUS_BASE_URL = "https://api.nebius.ai/v1"
backups/pre_test_backup_20250923_202627/streamlit_app.py ADDED
@@ -0,0 +1,891 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Care Count (Volunteers → Visits → Items) ---
2
+ # Modern UI/UX with industry-standard design patterns
3
+ # Enhanced version with improved user experience
4
+
5
+ from __future__ import annotations
6
+
7
+ import os, io, time, base64, re, uuid, json
8
+ from datetime import datetime, timedelta
9
+ from typing import Optional
10
+ import logging
11
+
12
+ import pytz
13
+ import requests
14
+ import pandas as pd
15
+ import streamlit as st
16
+ from PIL import Image, ImageOps, ImageEnhance
17
+ from supabase import create_client, Client
18
+
19
+ # Import our modern UI components
20
+ from ui_improvements import ModernUIComponents, apply_modern_ui, create_modern_layout
21
+
22
+ # ------------------------ Enhanced Logging ------------------------
23
+ logging.basicConfig(
24
+ level=logging.INFO,
25
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
26
+ handlers=[
27
+ logging.FileHandler('care_count.log'),
28
+ logging.StreamHandler()
29
+ ]
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # ------------------------ App config ------------------------
34
+ os.environ.setdefault("HOME", "/tmp")
35
+ os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
36
+
37
+ TZ = os.getenv("APP_TZ", "America/Toronto")
38
+ CUTOFF_HOUR = int(os.getenv("CUTOFF_HOUR", "20")) # 8pm local
39
+ INACTIVITY_MIN = int(os.getenv("INACTIVITY_MIN", "30"))
40
+
41
+ def local_now() -> datetime:
42
+ return datetime.now(pytz.timezone(TZ))
43
+
44
+ # Enhanced page config with modern settings
45
+ st.set_page_config(
46
+ page_title="Care Count - Volunteer Management",
47
+ page_icon="💜",
48
+ layout="wide",
49
+ initial_sidebar_state="expanded",
50
+ menu_items={
51
+ 'Get Help': 'https://github.com/your-repo/care-count',
52
+ 'Report a bug': "https://github.com/your-repo/care-count/issues",
53
+ 'About': "# Care Count\nVolunteer management system for community impact tracking"
54
+ }
55
+ )
56
+
57
+ # Apply modern UI styling
58
+ apply_modern_ui()
59
+
60
+ # ------------------------ Enhanced Secrets Management ------------------------
61
+ try:
62
+ from dotenv import load_dotenv
63
+ load_dotenv()
64
+ except Exception:
65
+ pass
66
+
67
+ try:
68
+ _SECRETS = getattr(st, "secrets", {})
69
+ except Exception:
70
+ _SECRETS = {}
71
+
72
+ def _is_useful(val: Optional[str]) -> bool:
73
+ return val is not None and str(val).strip() != ""
74
+
75
+ def get_secret(name: str, default: Optional[str] = None) -> Optional[str]:
76
+ """Enhanced secret management with logging"""
77
+ v = os.getenv(name)
78
+ if _is_useful(v):
79
+ logger.info(f"Secret {name} loaded from environment")
80
+ return v
81
+
82
+ try:
83
+ if hasattr(_SECRETS, "get"):
84
+ v = _SECRETS.get(name, default)
85
+ if _is_useful(v):
86
+ logger.info(f"Secret {name} loaded from Streamlit secrets")
87
+ return v
88
+ except Exception as e:
89
+ logger.warning(f"Failed to load secret {name} from Streamlit secrets: {e}")
90
+
91
+ return default
92
+
93
+ def require_secret(name: str) -> str:
94
+ """Enhanced secret requirement with better error handling"""
95
+ v = get_secret(name, None)
96
+ if not _is_useful(v):
97
+ st.error(
98
+ f"🔐 **Configuration Error**\n\n"
99
+ f"Missing required configuration: `{name}`\n\n"
100
+ f"Please ensure this is set in your environment variables or `.streamlit/secrets.toml` file.\n\n"
101
+ f"For help, check the [documentation](https://github.com/your-repo/care-count#configuration)."
102
+ )
103
+ st.stop()
104
+ return str(v)
105
+
106
+ # Required Supabase credentials
107
+ SUPABASE_URL = require_secret("SUPABASE_URL")
108
+ SUPABASE_KEY = require_secret("SUPABASE_KEY")
109
+
110
+ # Initialize Supabase client with error handling
111
+ try:
112
+ sb: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
113
+ logger.info("Supabase client initialized successfully")
114
+ except Exception as e:
115
+ st.error(f"Failed to connect to Supabase: {e}")
116
+ logger.error(f"Supabase connection failed: {e}")
117
+ st.stop()
118
+
119
+ # Optional provider/config
120
+ PROVIDER = (get_secret("PROVIDER", "nebius") or "nebius").strip().lower()
121
+ GEMMA_MODEL = (get_secret("GEMMA_MODEL", "google/gemma-3-27b-it") or "google/gemma-3-27b-it").strip()
122
+ NEBIUS_API_KEY = get_secret("NEBIUS_API_KEY")
123
+ NEBIUS_BASE_URL = get_secret("NEBIUS_BASE_URL", "https://api.nebius.ai/v1")
124
+ FEATH_API_KEY = get_secret("FEATHERLESS_API_KEY")
125
+ FEATH_BASE_URL = get_secret("FEATHERLESS_BASE_URL", "https://api.featherless.ai/v1")
126
+
127
+ # ------------------------ Enhanced Event Logging ------------------------
128
+ def log_event(action: str, actor: Optional[str], details: dict, level: str = "info"):
129
+ """Enhanced event logging with multiple levels"""
130
+ log_data = {
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "action": action,
133
+ "actor_email": actor,
134
+ "details": details,
135
+ "level": level
136
+ }
137
+
138
+ # Log to file
139
+ if level == "error":
140
+ logger.error(f"Event: {action} by {actor} - {details}")
141
+ elif level == "warning":
142
+ logger.warning(f"Event: {action} by {actor} - {details}")
143
+ else:
144
+ logger.info(f"Event: {action} by {actor} - {details}")
145
+
146
+ # Log to database
147
+ try:
148
+ sb.table("events").insert(log_data).execute()
149
+ except Exception as e:
150
+ logger.error(f"Failed to log event to database: {e}")
151
+
152
+ # ------------------------ Modern Authentication UI ------------------------
153
+ def auth_block() -> tuple[bool, Optional[str]]:
154
+ """Enhanced authentication with modern UI"""
155
+ if "auth_email" not in st.session_state:
156
+ st.session_state["auth_email"] = None
157
+ if "user_email" in st.session_state:
158
+ return True, st.session_state["user_email"]
159
+
160
+ # Modern hero section for login
161
+ st.markdown(ModernUIComponents.create_hero_section(
162
+ "Care Count",
163
+ "Thanks for showing up for the community today. Snap items, keep visits tidy, and help us understand the impact of your time."
164
+ ))
165
+
166
+ # Modern login form
167
+ with st.container():
168
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
169
+ st.subheader("🔐 Sign In")
170
+ st.markdown("Enter your email to receive a secure login code.")
171
+
172
+ with st.form("otp_request", clear_on_submit=False):
173
+ email = st.text_input(
174
+ "Email Address",
175
+ value=st.session_state.get("auth_email") or "",
176
+ placeholder="your.email@example.com",
177
+ help="We'll send you a secure 6-digit code"
178
+ )
179
+
180
+ col1, col2, col3 = st.columns([1, 2, 1])
181
+ with col2:
182
+ send = st.form_submit_button("📧 Send Login Code", use_container_width=True)
183
+
184
+ if send:
185
+ if not email or "@" not in email:
186
+ st.error("Please enter a valid email address.")
187
+ else:
188
+ try:
189
+ with st.spinner("Sending login code..."):
190
+ sb.auth.sign_in_with_otp({"email": email, "shouldCreateUser": True})
191
+ st.session_state["auth_email"] = email
192
+ st.success("✅ Login code sent! Check your email.")
193
+ log_event("otp_requested", email, {"method": "email"})
194
+ except Exception as e:
195
+ st.error(f"❌ Could not send code: {e}")
196
+ log_event("otp_failed", email, {"error": str(e)}, "error")
197
+
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # OTP verification form
201
+ if st.session_state.get("auth_email"):
202
+ with st.container():
203
+ st.markdown('<div class="modern-card">', unsafe_allow_html=True)
204
+ st.subheader("🔢 Verify Code")
205
+ st.markdown(f"Enter the 6-digit code sent to **{st.session_state['auth_email']}**")
206
+
207
+ with st.form("otp_verify", clear_on_submit=True):
208
+ code = st.text_input(
209
+ "Verification Code",
210
+ max_chars=6,
211
+ placeholder="123456",
212
+ help="Enter the 6-digit code from your email"
213
+ )
214
+
215
+ col1, col2, col3 = st.columns([1, 2, 1])
216
+ with col2:
217
+ ok = st.form_submit_button("✅ Verify & Start Shift", use_container_width=True)
218
+
219
+ if ok:
220
+ if len(code) != 6 or not code.isdigit():
221
+ st.error("Please enter a valid 6-digit code.")
222
+ else:
223
+ try:
224
+ with st.spinner("Verifying code..."):
225
+ res = sb.auth.verify_otp({"email": st.session_state["auth_email"], "token": code, "type": "email"})
226
+ if res and res.user:
227
+ email = st.session_state["auth_email"]
228
+
229
+ # Enhanced volunteer upsert
230
+ volunteer_data = {
231
+ "email": email,
232
+ "last_login_at": datetime.utcnow().isoformat(),
233
+ "shift_started_at": datetime.utcnow().isoformat(),
234
+ "shift_ended_at": None,
235
+ "login_count": 1 # Track login frequency
236
+ }
237
+
238
+ sb.table("volunteers").upsert(volunteer_data, on_conflict="email").execute()
239
+
240
+ st.session_state["user_email"] = email
241
+ st.session_state["shift_started"] = True
242
+ st.session_state["last_activity_at"] = local_now()
243
+
244
+ log_event("login_success", email, {"method": "otp"})
245
+ st.success("🎉 Welcome back! Your shift has started.")
246
+ st.balloons()
247
+ return True, email
248
+ except Exception as e:
249
+ st.error(f"❌ Verification failed: {e}")
250
+ log_event("otp_verification_failed", st.session_state["auth_email"], {"error": str(e)}, "error")
251
+
252
+ st.markdown('</div>', unsafe_allow_html=True)
253
+
254
+ return False, None
255
+
256
+ def end_shift(email: str, reason: str):
257
+ """Enhanced shift ending with better logging"""
258
+ try:
259
+ end_time = datetime.utcnow().isoformat()
260
+ sb.table("volunteers").update({"shift_ended_at": end_time}).eq("email", email).execute()
261
+ log_event("shift_end", email, {"reason": reason, "end_time": end_time})
262
+ except Exception as e:
263
+ logger.error(f"Failed to end shift for {email}: {e}")
264
+
265
+ def guard_cutoff_and_idle(email: str):
266
+ """Enhanced session management with better UX"""
267
+ now = local_now()
268
+ last = st.session_state.get("last_activity_at")
269
+
270
+ if last and (now - last).total_seconds() > INACTIVITY_MIN * 60:
271
+ end_shift(email, "inactivity")
272
+ st.session_state.clear()
273
+ st.warning("⏰ You were logged out due to inactivity. Thank you for volunteering today!")
274
+ st.stop()
275
+
276
+ st.session_state["last_activity_at"] = now
277
+
278
+ cutoff = now.replace(hour=CUTOFF_HOUR, minute=0, second=0, microsecond=0)
279
+ if now >= cutoff:
280
+ end_shift(email, "cutoff_8pm")
281
+ st.session_state.clear()
282
+ st.info("🌅 We close the day at 8pm. Your shift has been ended. Thank you so much!")
283
+ st.stop()
284
+
285
+ # ------------------------ Main App Flow ------------------------
286
+ def main():
287
+ """Main application flow with modern UI"""
288
+
289
+ # Authentication
290
+ signed_in, user_email = auth_block()
291
+ if not signed_in:
292
+ st.stop()
293
+
294
+ guard_cutoff_and_idle(user_email)
295
+
296
+ # Modern welcome section
297
+ st.markdown(ModernUIComponents.create_hero_section(
298
+ "Care Count Dashboard",
299
+ "Manage visits, track items, and make a difference in your community.",
300
+ user_email
301
+ ))
302
+
303
+ # Enhanced status cards
304
+ vrow = fetch_volunteer_row(user_email) or {}
305
+ shift_started_at = vrow.get("shift_started_at")
306
+ lifetime_hours = vrow.get("total_hours", 0)
307
+
308
+ mins_active = 0
309
+ try:
310
+ if shift_started_at:
311
+ t0 = datetime.fromisoformat(str(shift_started_at).replace("Z","+00:00"))
312
+ mins_active = max(0, int((datetime.utcnow() - t0).total_seconds() // 60))
313
+ except Exception:
314
+ pass
315
+
316
+ status_data = {
317
+ "shift_active": f"{mins_active} min",
318
+ "items_today": items_today(user_email),
319
+ "lifetime_hours": f"{round(float(lifetime_hours), 1)} hrs" if isinstance(lifetime_hours, (int, float)) else "0.0 hrs"
320
+ }
321
+
322
+ st.markdown(ModernUIComponents.create_status_cards(status_data), unsafe_allow_html=True)
323
+
324
+ # Modern sign-out button
325
+ col1, col2, col3 = st.columns([1, 1, 1])
326
+ with col2:
327
+ if st.button("🔒 Sign Out", use_container_width=True, type="secondary"):
328
+ end_shift(user_email, "manual")
329
+ st.session_state.clear()
330
+ st.success("✅ Signed out successfully. See you next time!")
331
+ st.rerun()
332
+
333
+ # Enhanced visit management section
334
+ st.markdown(ModernUIComponents.create_modern_form_section(
335
+ "🪪 Visit Management",
336
+ "Start and manage student visits with unique tracking codes"
337
+ ), unsafe_allow_html=True)
338
+
339
+ active_visit = st.session_state.get("active_visit")
340
+
341
+ col1, col2, col3 = st.columns([1, 1, 1])
342
+
343
+ with col1:
344
+ if not active_visit and st.button("🚀 Start New Visit", use_container_width=True, type="primary"):
345
+ try:
346
+ with st.spinner("Creating visit..."):
347
+ payload = {
348
+ "visit_code": fallback_visit_code(),
349
+ "started_at": datetime.utcnow().isoformat(),
350
+ "ended_at": None,
351
+ "created_by": user_email
352
+ }
353
+ v = sb.table("visits").insert(payload).execute().data[0]
354
+ if not v.get("visit_code"):
355
+ v["visit_code"] = payload["visit_code"]
356
+ st.session_state["active_visit"] = v
357
+ st.success(f"✅ Visit #{v['id']} started")
358
+ st.info(f"**Visit Code:** `{v['visit_code']}`")
359
+ log_event("visit_start", user_email, {"visit_id": v["id"], "visit_code": v["visit_code"]})
360
+ st.rerun()
361
+ except Exception as e:
362
+ st.error(f"❌ Could not start visit: {e}")
363
+ log_event("visit_start_failed", user_email, {"error": str(e)}, "error")
364
+
365
+ with col2:
366
+ if active_visit and st.button("🏁 End Visit", use_container_width=True, type="secondary"):
367
+ try:
368
+ with st.spinner("Ending visit..."):
369
+ sb.table("visits").update({"ended_at": datetime.utcnow().isoformat()}) \
370
+ .eq("id", active_visit["id"]).execute()
371
+ st.success("✅ Visit completed successfully")
372
+ log_event("visit_end", user_email, {"visit_id": active_visit["id"]})
373
+ st.session_state.pop("active_visit", None)
374
+ st.rerun()
375
+ except Exception as e:
376
+ st.error(f"❌ Could not end visit: {e}")
377
+ log_event("visit_end_failed", user_email, {"error": str(e)}, "error")
378
+
379
+ with col3:
380
+ if st.session_state.get("active_visit"):
381
+ v = st.session_state["active_visit"]
382
+ st.info(f"**Active Visit:** #{v['id']}\n**Code:** {v.get('visit_code','')}")
383
+ else:
384
+ st.info("No active visit")
385
+
386
+ st.markdown('</div>', unsafe_allow_html=True)
387
+
388
+ # Enhanced item identification section
389
+ st.markdown(ModernUIComponents.create_modern_form_section(
390
+ "📸 Item Identification",
391
+ "Use AI-powered image recognition to identify items quickly and accurately"
392
+ ), unsafe_allow_html=True)
393
+
394
+ col1, col2 = st.columns(2)
395
+
396
+ with col1:
397
+ st.subheader("📷 Camera Capture")
398
+ cam = st.camera_input("Take a photo of the item", help="Position the item clearly in the frame")
399
+
400
+ with col2:
401
+ st.subheader("📁 File Upload")
402
+ up = st.file_uploader(
403
+ "Upload an image",
404
+ type=["png","jpg","jpeg"],
405
+ help="Supported formats: PNG, JPG, JPEG"
406
+ )
407
+
408
+ img_file = cam or up
409
+ if img_file:
410
+ try:
411
+ img = Image.open(img_file).convert("RGB")
412
+ st.image(img, use_container_width=True, caption="Captured Image")
413
+
414
+ if st.button("🔍 Identify Item with AI", use_container_width=True, type="primary"):
415
+ with st.spinner("Analyzing image with AI..."):
416
+ t0 = time.time()
417
+ try:
418
+ pre = preprocess_for_label(img)
419
+ raw = gemma_item_name(_to_png_bytes(pre))
420
+ processing_time = time.time() - t0
421
+
422
+ norm = normalize_item_name(raw)
423
+
424
+ if raw:
425
+ st.success(f"🤖 **AI Detection:** {raw}")
426
+ st.info(f"✨ **Normalized:** {norm or '(unknown)'} · ⏱️ {processing_time:.2f}s")
427
+
428
+ st.session_state["scanned_item_name"] = norm or raw or ""
429
+ st.session_state["last_activity_at"] = local_now()
430
+
431
+ log_event("item_identified", user_email, {
432
+ "raw_name": raw,
433
+ "normalized_name": norm,
434
+ "processing_time": processing_time
435
+ })
436
+
437
+ except Exception as e:
438
+ st.error(f"❌ AI identification failed: {e}")
439
+ log_event("ai_identification_failed", user_email, {"error": str(e)}, "error")
440
+ except Exception as e:
441
+ st.error(f"❌ Failed to process image: {e}")
442
+ log_event("image_processing_failed", user_email, {"error": str(e)}, "error")
443
+
444
+ st.markdown('</div>', unsafe_allow_html=True)
445
+
446
+ # Enhanced item logging section
447
+ st.markdown(ModernUIComponents.create_modern_form_section(
448
+ "📬 Item Logging",
449
+ "Log items to the current visit with detailed information"
450
+ ), unsafe_allow_html=True)
451
+
452
+ col1, col2 = st.columns([2, 1])
453
+
454
+ with col1:
455
+ item_name = st.text_input(
456
+ "Item Name",
457
+ value=st.session_state.get("scanned_item_name",""),
458
+ placeholder="Enter item name or use AI detection above",
459
+ help="Required field - item name for tracking"
460
+ )
461
+
462
+ with col2:
463
+ quantity = st.number_input(
464
+ "Quantity",
465
+ min_value=1,
466
+ max_value=9999,
467
+ step=1,
468
+ value=1,
469
+ help="Number of items"
470
+ )
471
+
472
+ col3, col4 = st.columns(2)
473
+
474
+ with col3:
475
+ category = st.text_input(
476
+ "Category (optional)",
477
+ placeholder="e.g., Food, Hygiene, Clothing",
478
+ help="Item category for better organization"
479
+ )
480
+
481
+ with col4:
482
+ unit = st.text_input(
483
+ "Unit (optional)",
484
+ placeholder="e.g., 500 mL, 1 L, 250 g",
485
+ help="Unit of measurement"
486
+ )
487
+
488
+ barcode = st.text_input(
489
+ "Barcode (optional)",
490
+ placeholder="Scan or enter barcode",
491
+ help="Product barcode for inventory tracking"
492
+ )
493
+
494
+ save_disabled = not st.session_state.get("active_visit")
495
+ if st.button("✅ Save Item to Visit", disabled=save_disabled, use_container_width=True, type="primary"):
496
+ v = st.session_state.get("active_visit")
497
+ if not v:
498
+ st.warning("⚠️ Please start a visit first before logging items.")
499
+ else:
500
+ name_clean = clean_text(item_name, 120)
501
+ if not name_clean:
502
+ st.warning("⚠️ Item name is required.")
503
+ else:
504
+ with st.spinner("Saving item..."):
505
+ ts_iso = datetime.utcnow().isoformat()
506
+ ingest_id = deterministic_ingest_id(int(v["id"]), user_email, name_clean, int(quantity), ts_iso)
507
+
508
+ try:
509
+ ok, msg = try_rpc_ingest(
510
+ email=user_email, v_id=int(v["id"]), name=name_clean, qty=int(quantity),
511
+ category=clean_text(category, 80), unit=clean_text(unit, 40),
512
+ barcode=clean_text(barcode, 64), ts_iso=ts_iso, ingest_id=ingest_id
513
+ )
514
+
515
+ if ok:
516
+ st.success("✅ Item logged successfully!")
517
+ log_event("item_logged", user_email, {
518
+ "visit_id": v["id"],
519
+ "item_name": name_clean,
520
+ "quantity": quantity
521
+ })
522
+ else:
523
+ st.warning(f"⚠️ {msg}. Trying fallback method...")
524
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
525
+ clean_text(category,80), clean_text(unit,40),
526
+ clean_text(barcode,64), ts_iso, ingest_id)
527
+ st.success("✅ Item logged successfully (fallback method)!")
528
+ log_event("item_logged_fallback", user_email, {
529
+ "visit_id": v["id"],
530
+ "item_name": name_clean,
531
+ "quantity": quantity
532
+ })
533
+ except Exception as e:
534
+ try:
535
+ fallback_direct_insert(user_email, int(v["id"]), name_clean, int(quantity),
536
+ clean_text(category,80), clean_text(unit,40),
537
+ clean_text(barcode,64), ts_iso, ingest_id)
538
+ st.success("✅ Item logged successfully (fallback method)!")
539
+ log_event("item_logged_fallback", user_email, {
540
+ "visit_id": v["id"],
541
+ "item_name": name_clean,
542
+ "quantity": quantity
543
+ })
544
+ except Exception as e2:
545
+ st.error(f"❌ Failed to log item: {e2}")
546
+ log_event("item_log_failed", user_email, {"error": str(e2)}, "error")
547
+
548
+ st.session_state["last_activity_at"] = local_now()
549
+ st.session_state["scanned_item_name"] = name_clean
550
+ st.rerun()
551
+
552
+ st.markdown('</div>', unsafe_allow_html=True)
553
+
554
+ # Enhanced visit items view
555
+ if st.session_state.get("active_visit"):
556
+ st.markdown(ModernUIComponents.create_modern_form_section(
557
+ "🧾 Current Visit Items",
558
+ "Review and manage items in the current visit"
559
+ ), unsafe_allow_html=True)
560
+
561
+ rows = load_items_for_visit(int(st.session_state["active_visit"]["id"]))
562
+ if rows:
563
+ df = pd.DataFrame(rows)
564
+
565
+ # Enhanced dataframe display
566
+ st.dataframe(
567
+ df,
568
+ use_container_width=True,
569
+ hide_index=True,
570
+ column_config={
571
+ "timestamp": st.column_config.DatetimeColumn("Time", format="MM/DD/YY HH:mm"),
572
+ "item_name": st.column_config.TextColumn("Item Name", width="medium"),
573
+ "qty": st.column_config.NumberColumn("Quantity", width="small"),
574
+ "category": st.column_config.TextColumn("Category", width="small"),
575
+ "unit": st.column_config.TextColumn("Unit", width="small")
576
+ }
577
+ )
578
+
579
+ # Enhanced delete functionality
580
+ with st.expander("🗑️ Delete Item (if mis-logged)"):
581
+ if rows:
582
+ item_options = {f"{r['item_name']} (Qty: {r['qty']})": r["id"] for r in rows if "id" in r}
583
+ selected_item = st.selectbox("Select item to delete", list(item_options.keys()))
584
+
585
+ if st.button("🗑️ Delete Selected Item", type="secondary"):
586
+ item_id = item_options[selected_item]
587
+ try:
588
+ # Try both tables safely
589
+ try:
590
+ delete_item("visit_items_p", int(item_id))
591
+ except Exception:
592
+ delete_item("visit_items", int(item_id))
593
+
594
+ st.success("✅ Item deleted successfully")
595
+ log_event("item_deleted", user_email, {"item_id": item_id})
596
+ st.rerun()
597
+ except Exception as e:
598
+ st.error(f"❌ Failed to delete item: {e}")
599
+ log_event("item_delete_failed", user_email, {"error": str(e)}, "error")
600
+ else:
601
+ st.info("No items to delete")
602
+ else:
603
+ st.info("📝 No items logged for this visit yet.")
604
+
605
+ st.markdown('</div>', unsafe_allow_html=True)
606
+
607
+ # Enhanced analytics section
608
+ st.markdown(ModernUIComponents.create_modern_form_section(
609
+ "📈 Today's Analytics",
610
+ "Real-time insights into today's volunteer activity"
611
+ ), unsafe_allow_html=True)
612
+
613
+ try:
614
+ daily = sb.table("v_daily_activity").select("*").execute().data
615
+ today_str = local_now().strftime("%Y-%m-%d")
616
+ today = next((r for r in daily if str(r.get("day",""))[:10]==today_str), None)
617
+
618
+ if today:
619
+ visits = int(today.get("visits", 0))
620
+ items = int(today.get("items", 0))
621
+
622
+ col1, col2 = st.columns(2)
623
+ with col1:
624
+ st.metric("Total Visits Today", visits, delta=None)
625
+ with col2:
626
+ st.metric("Total Items Processed", items, delta=None)
627
+
628
+ # Progress indicator
629
+ if visits > 0:
630
+ st.markdown(ModernUIComponents.create_progress_indicator(
631
+ items, visits * 10, "Items per Visit Target"
632
+ ), unsafe_allow_html=True)
633
+ else:
634
+ st.info("📊 Analytics data will appear here as activity increases.")
635
+
636
+ except Exception as e:
637
+ st.info("📊 Analytics view will be available once the database view is set up.")
638
+ logger.warning(f"Analytics view not available: {e}")
639
+
640
+ st.markdown('</div>', unsafe_allow_html=True)
641
+
642
+ # Enhanced footer with helpful information
643
+ st.markdown("""
644
+ <div class="modern-card" style="margin-top: var(--space-8); text-align: center;">
645
+ <h4>💡 Quick Tips</h4>
646
+ <p style="color: var(--gray-400); margin: 0;">
647
+ When you're done, please <strong>End Visit</strong> and <strong>Sign out</strong>.<br>
648
+ We'll auto-sign you out after inactivity or at 8pm local time. 💜
649
+ </p>
650
+ </div>
651
+ """, unsafe_allow_html=True)
652
+
653
+ # ------------------------ Helper Functions (Enhanced) ------------------------
654
+ def fetch_volunteer_row(email: str) -> dict | None:
655
+ """Enhanced volunteer data fetching with error handling"""
656
+ try:
657
+ return sb.table("volunteers").select("*").eq("email", email).single().execute().data
658
+ except Exception as e:
659
+ logger.warning(f"Failed to fetch volunteer data for {email}: {e}")
660
+ return None
661
+
662
+ def items_today(email: str) -> int:
663
+ """Enhanced item counting with better error handling"""
664
+ try:
665
+ day = local_now().strftime("%Y-%m-%d")
666
+ # Try partitioned table first
667
+ data = sb.table("visit_items_p").select("id,timestamp,volunteer") \
668
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
669
+ .eq("volunteer", email).execute().data
670
+ return len(data or [])
671
+ except Exception:
672
+ try:
673
+ data = sb.table("visit_items").select("id,timestamp,volunteer") \
674
+ .gte("timestamp", f"{day} 00:00:00").lte("timestamp", f"{day} 23:59:59") \
675
+ .eq("volunteer", email).execute().data
676
+ return len(data or [])
677
+ except Exception as e:
678
+ logger.warning(f"Failed to count items for {email}: {e}")
679
+ return 0
680
+
681
+ def fallback_visit_code() -> str:
682
+ """Enhanced visit code generation with better uniqueness"""
683
+ try:
684
+ day = local_now().strftime("%Y-%m-%d")
685
+ todays = sb.table("visits").select("id,started_at").gte("started_at", f"{day} 00:00:00") \
686
+ .lte("started_at", f"{day} 23:59:59").execute().data or []
687
+ seq = len(todays) + 1
688
+ except Exception:
689
+ seq = int(time.time()) % 1000
690
+ return f"V-{seq}-{local_now().strftime('%Y%m%d')}-{str(uuid.uuid4())[:6]}"
691
+
692
+ def _to_png_bytes(img: Image.Image) -> bytes:
693
+ """Convert image to PNG bytes"""
694
+ b = io.BytesIO()
695
+ img.save(b, format="PNG")
696
+ return b.getvalue()
697
+
698
+ def preprocess_for_label(img: Image.Image) -> Image.Image:
699
+ """Enhanced image preprocessing for better AI recognition"""
700
+ img = img.convert("RGB")
701
+ w, h = img.size
702
+ scale = 1024 / max(w, h) if max(w, h) > 1024 else 1.0
703
+ if scale < 1.0:
704
+ img = img.resize((int(w * scale), int(h * scale)))
705
+ img = ImageOps.autocontrast(img, cutoff=2)
706
+ img = ImageEnhance.Brightness(img).enhance(1.06)
707
+ img = ImageEnhance.Contrast(img).enhance(1.05)
708
+ img = ImageEnhance.Sharpness(img).enhance(1.15)
709
+ return img
710
+
711
+ def gemma_item_name(img_bytes: bytes) -> str:
712
+ """Enhanced AI item identification with better error handling"""
713
+ try:
714
+ if PROVIDER == "nebius":
715
+ if not NEBIUS_API_KEY:
716
+ raise RuntimeError("NEBIUS_API_KEY missing")
717
+ return _openai_style_chat(NEBIUS_BASE_URL, NEBIUS_API_KEY, GEMMA_MODEL, img_bytes)
718
+ elif PROVIDER == "featherless":
719
+ if not FEATH_API_KEY:
720
+ raise RuntimeError("FEATHERLESS_API_KEY missing")
721
+ return _openai_style_chat(FEATH_BASE_URL, FEATH_API_KEY, GEMMA_MODEL, img_bytes)
722
+ else:
723
+ raise RuntimeError(f"Unknown PROVIDER: {PROVIDER}")
724
+ except Exception as e:
725
+ logger.error(f"AI identification failed: {e}")
726
+ raise
727
+
728
+ def _openai_style_chat(base_url: str, api_key: str, model_id: str, img_bytes: bytes) -> str:
729
+ """Enhanced API communication with better error handling"""
730
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
731
+ url = f"{base_url.rstrip('/')}/chat/completions"
732
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
733
+ payload = {
734
+ "model": model_id,
735
+ "temperature": 0,
736
+ "messages": [
737
+ {"role": "system", "content": "You label item being held in the image for a food bank. Return ONLY the item name."},
738
+ {"role": "user", "content": [
739
+ {"type": "text", "text": "What is the name of the item in the picture? Return only the item name."},
740
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}
741
+ ]}
742
+ ]
743
+ }
744
+
745
+ try:
746
+ r = requests.post(url, json=payload, headers=headers, timeout=90)
747
+ if r.status_code != 200:
748
+ raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")
749
+ data = r.json()
750
+ return (data["choices"][0]["message"]["content"] or "").strip()
751
+ except requests.exceptions.Timeout:
752
+ raise RuntimeError("AI service timeout - please try again")
753
+ except requests.exceptions.RequestException as e:
754
+ raise RuntimeError(f"AI service error: {e}")
755
+
756
+ def normalize_item_name(s: str) -> str:
757
+ """Enhanced item name normalization"""
758
+ s = (s or "").strip()
759
+ if not s:
760
+ return ""
761
+
762
+ # Enhanced brand and type recognition
763
+ BRANDS = {
764
+ "whiskas","tetley","kellogg's","kelloggs","campbell's","campbells","heinz",
765
+ "nestle","kraft","general mills","cheerios","oreo","oreos","pringles","lays","doritos",
766
+ "ice river","green bottle","great value","wheat thins","vegetable thins","raid"
767
+ }
768
+ GENERIC_TYPES = {
769
+ "water","toothpaste","deodorant","antiperspirant","soap","shampoo","conditioner",
770
+ "lotion","tea","coffee","cereal","pasta","rice","beans","sauce","salsa","cleaner",
771
+ "peanut butter","jam","jelly","tuna","chicken","beef","flour","sugar","salt","oil",
772
+ "crackers","cookies","soup","insect killer","spray"
773
+ }
774
+
775
+ low = re.sub(r"[®™]", "", s.lower())
776
+ for b in BRANDS:
777
+ low = low.replace(b, "")
778
+
779
+ chosen = None
780
+ for t in GENERIC_TYPES:
781
+ if t in low:
782
+ chosen = t
783
+ break
784
+
785
+ cleaned = " ".join(low.split())
786
+ return (chosen or cleaned.title())[:120]
787
+
788
+ def clean_text(v: Optional[str], maxlen: int = 120) -> Optional[str]:
789
+ """Enhanced text cleaning"""
790
+ if not v:
791
+ return None
792
+ v = re.sub(r"\s+", " ", v).strip()
793
+ return v[:maxlen] if v else None
794
+
795
+ def deterministic_ingest_id(v_id: int, email: str, name: str, qty: int, ts_iso: str) -> str:
796
+ """Enhanced ID generation for data integrity"""
797
+ key = f"visit_items::{v_id}::{email}::{name}::{qty}::{ts_iso}"
798
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, key))
799
+
800
+ def try_rpc_ingest(email: str, v_id: int, name: str, qty: int,
801
+ category: Optional[str], unit: Optional[str],
802
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> tuple[bool,str]:
803
+ """Enhanced RPC ingestion with better error handling"""
804
+ try:
805
+ res = sb.rpc("safe_ingest_visit_item", {
806
+ "p_email": email,
807
+ "p_visit_id": v_id,
808
+ "p_item_name": name,
809
+ "p_qty": qty,
810
+ "p_category": category,
811
+ "p_unit": unit,
812
+ "p_barcode": barcode,
813
+ "p_ts": ts_iso,
814
+ "p_ingest_id": ingest_id
815
+ }).execute()
816
+
817
+ rows = res.data if isinstance(res.data, list) else []
818
+ if rows:
819
+ r0 = rows[0]
820
+ ok = bool(r0.get("ok", False))
821
+ msg = str(r0.get("msg", ""))
822
+ return ok, msg or ("ok" if ok else "failed")
823
+ return True, "ok"
824
+ except Exception as e:
825
+ logger.warning(f"RPC ingest failed, using fallback: {e}")
826
+ raise e
827
+
828
+ def fallback_direct_insert(email: str, v_id: int, name: str, qty: int,
829
+ category: Optional[str], unit: Optional[str],
830
+ barcode: Optional[str], ts_iso: str, ingest_id: str) -> None:
831
+ """Enhanced fallback insertion with better error handling"""
832
+ payload = {
833
+ "visit_id": v_id,
834
+ "timestamp": ts_iso,
835
+ "volunteer": email,
836
+ "item_name": name,
837
+ "category": category,
838
+ "unit": unit,
839
+ "qty": qty,
840
+ "barcode": barcode,
841
+ "weather_type": None,
842
+ "temp_c": None,
843
+ "ingest_id": ingest_id
844
+ }
845
+
846
+ try:
847
+ sb.table("visit_items_p").insert(payload).execute()
848
+ except Exception:
849
+ # Legacy table fallback
850
+ payload.pop("ingest_id", None)
851
+ sb.table("visit_items").insert(payload).execute()
852
+
853
+ def load_items_for_visit(visit_id: int) -> list[dict]:
854
+ """Enhanced item loading with better error handling"""
855
+ try:
856
+ return sb.table("visit_items_p").select("*").eq("visit_id", visit_id) \
857
+ .order("timestamp", desc=True).limit(500).execute().data or []
858
+ except Exception:
859
+ try:
860
+ return sb.table("visit_items").select("*").eq("visit_id", visit_id) \
861
+ .order("timestamp", desc=True).limit(500).execute().data or []
862
+ except Exception as e:
863
+ logger.warning(f"Failed to load items for visit {visit_id}: {e}")
864
+ return []
865
+
866
+ def delete_item(table: str, item_id: int):
867
+ """Enhanced item deletion with better error handling"""
868
+ try:
869
+ sb.table(table).delete().eq("id", item_id).execute()
870
+ except Exception as e:
871
+ logger.error(f"Failed to delete item {item_id} from {table}: {e}")
872
+ raise e
873
+
874
+ # ------------------------ App Configuration Display ------------------------
875
+ st.sidebar.markdown("### ⚙️ Configuration")
876
+ st.sidebar.info(f"""
877
+ **Provider:** `{PROVIDER}`
878
+ **Model:** `{GEMMA_MODEL}`
879
+ **Timezone:** `{TZ}`
880
+ **Cutoff:** {CUTOFF_HOUR}:00 PM
881
+ **Inactivity:** {INACTIVITY_MIN} min
882
+ """)
883
+
884
+ # ------------------------ Main App Execution ------------------------
885
+ if __name__ == "__main__":
886
+ try:
887
+ main()
888
+ except Exception as e:
889
+ logger.error(f"Application error: {e}")
890
+ st.error(f"❌ Application error: {e}")
891
+ st.info("Please refresh the page or contact support if the issue persists.")