mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-24 20:51:09 -05:00
Add secret management guide, client-side performance monitoring, and search accessibility enhancements
- Introduced a comprehensive Secret Management Guide detailing best practices, secret classification, development setup, production management, rotation procedures, and emergency protocols. - Implemented a client-side performance monitoring script to track various metrics including page load performance, paint metrics, layout shifts, and memory usage. - Enhanced search accessibility with keyboard navigation support for search results, ensuring compliance with WCAG standards and improving user experience.
This commit is contained in:
226
docs/architecture/adr-006-media-handling-cloudflare.md
Normal file
226
docs/architecture/adr-006-media-handling-cloudflare.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# ADR-006: Media Handling with Cloudflare Images
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
ThrillWiki handles user-uploaded images for:
|
||||
- Park photos
|
||||
- Ride photos
|
||||
- User avatars
|
||||
|
||||
We needed a media handling solution that would:
|
||||
- Handle image uploads efficiently
|
||||
- Optimize images for different devices
|
||||
- Provide CDN delivery globally
|
||||
- Support image transformations (resizing, cropping)
|
||||
- Minimize storage costs
|
||||
|
||||
## Decision
|
||||
|
||||
We chose **Cloudflare Images** as our image hosting and transformation service.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ User Upload │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Django Backend │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ MediaService │ │
|
||||
│ │ - Validate upload │ │
|
||||
│ │ - Generate upload URL │ │
|
||||
│ │ - Store Cloudflare ID │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Cloudflare Images │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ - Image storage │ │
|
||||
│ │ - On-the-fly transformations │ │
|
||||
│ │ - Global CDN delivery │ │
|
||||
│ │ - Multiple variants │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Image Variants
|
||||
|
||||
| Variant | Dimensions | Use Case |
|
||||
|---------|------------|----------|
|
||||
| `thumbnail` | 150x150 | List views, avatars |
|
||||
| `card` | 400x300 | Card components |
|
||||
| `hero` | 1200x600 | Park/ride headers |
|
||||
| `public` | Original (max 2000px) | Full size view |
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
# Environment variables
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_ID = config('CLOUDFLARE_IMAGES_ACCOUNT_ID')
|
||||
CLOUDFLARE_IMAGES_API_TOKEN = config('CLOUDFLARE_IMAGES_API_TOKEN')
|
||||
CLOUDFLARE_IMAGES_ACCOUNT_HASH = config('CLOUDFLARE_IMAGES_ACCOUNT_HASH')
|
||||
```
|
||||
|
||||
### Upload Flow
|
||||
|
||||
```python
|
||||
# backend/apps/core/services/media_service.py
|
||||
class CloudflareImagesService:
|
||||
"""Service for handling image uploads to Cloudflare Images."""
|
||||
|
||||
def get_direct_upload_url(self):
|
||||
"""Get a one-time upload URL for direct uploads."""
|
||||
response = self._api_request('POST', 'direct_upload')
|
||||
return {
|
||||
'upload_url': response['uploadURL'],
|
||||
'image_id': response['id'],
|
||||
}
|
||||
|
||||
def get_image_url(self, image_id, variant='public'):
|
||||
"""Get the delivery URL for an image."""
|
||||
return f"https://imagedelivery.net/{self.account_hash}/{image_id}/{variant}"
|
||||
|
||||
def delete_image(self, image_id):
|
||||
"""Delete an image from Cloudflare."""
|
||||
return self._api_request('DELETE', f'images/{image_id}')
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Global CDN**: Fast image delivery worldwide
|
||||
2. **Automatic Optimization**: WebP/AVIF conversion as appropriate
|
||||
3. **On-Demand Transformations**: Resize/crop without pre-generating
|
||||
4. **Cost Effective**: Pay per image stored, not per transformation
|
||||
5. **Reduced Server Load**: Direct uploads bypass our servers
|
||||
6. **Security**: Signed URLs for private content if needed
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **Vendor Lock-in**: Cloudflare-specific API
|
||||
2. **External Dependency**: Service availability dependency
|
||||
3. **Cost Scaling**: Costs increase with storage volume
|
||||
4. **Migration Complexity**: Moving away requires re-uploading
|
||||
|
||||
### URL Structure
|
||||
|
||||
```
|
||||
https://imagedelivery.net/{account_hash}/{image_id}/{variant}
|
||||
|
||||
Examples:
|
||||
https://imagedelivery.net/abc123/img-xyz/thumbnail
|
||||
https://imagedelivery.net/abc123/img-xyz/hero
|
||||
https://imagedelivery.net/abc123/img-xyz/public
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Django + S3 + CloudFront
|
||||
|
||||
**Rejected because:**
|
||||
- Requires pre-generating all sizes
|
||||
- More complex infrastructure
|
||||
- Higher storage costs for variants
|
||||
- Manual CDN configuration
|
||||
|
||||
### Self-Hosted with ImageMagick
|
||||
|
||||
**Rejected because:**
|
||||
- Server CPU overhead for transformations
|
||||
- No built-in CDN
|
||||
- Storage management complexity
|
||||
- Scaling challenges
|
||||
|
||||
### Imgix
|
||||
|
||||
**Rejected because:**
|
||||
- Higher costs for our volume
|
||||
- Already using Cloudflare for other services
|
||||
- Similar feature set to Cloudflare Images
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Photo Model
|
||||
|
||||
```python
|
||||
class Photo(models.Model):
|
||||
"""Base photo model using Cloudflare Images."""
|
||||
|
||||
cloudflare_id = models.CharField(max_length=100, unique=True)
|
||||
original_filename = models.CharField(max_length=255)
|
||||
uploaded_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||
is_approved = models.BooleanField(default=False)
|
||||
caption = models.TextField(blank=True)
|
||||
|
||||
@property
|
||||
def thumbnail_url(self):
|
||||
return get_image_url(self.cloudflare_id, 'thumbnail')
|
||||
|
||||
@property
|
||||
def public_url(self):
|
||||
return get_image_url(self.cloudflare_id, 'public')
|
||||
```
|
||||
|
||||
### Direct Upload API
|
||||
|
||||
```python
|
||||
# backend/apps/api/v1/views/upload.py
|
||||
class DirectUploadView(APIView):
|
||||
"""Get a direct upload URL for Cloudflare Images."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
service = CloudflareImagesService()
|
||||
upload_data = service.get_direct_upload_url()
|
||||
return Response({
|
||||
'upload_url': upload_data['upload_url'],
|
||||
'image_id': upload_data['image_id'],
|
||||
})
|
||||
```
|
||||
|
||||
### Cleanup Task
|
||||
|
||||
```python
|
||||
# Celery task for cleaning up orphaned images
|
||||
@shared_task
|
||||
def cleanup_orphaned_images():
|
||||
"""Delete images not referenced by any model."""
|
||||
cutoff = timezone.now() - timedelta(hours=24)
|
||||
orphaned = CloudflareImage.objects.filter(
|
||||
created_at__lt=cutoff,
|
||||
park_photos__isnull=True,
|
||||
ride_photos__isnull=True,
|
||||
user_avatars__isnull=True,
|
||||
)
|
||||
for image in orphaned:
|
||||
service.delete_image(image.cloudflare_id)
|
||||
image.delete()
|
||||
```
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
```python
|
||||
def get_image_url(image_id, variant='public', fallback='/static/images/placeholder.jpg'):
|
||||
"""Get image URL with fallback for missing images."""
|
||||
if not image_id:
|
||||
return fallback
|
||||
return f"https://imagedelivery.net/{ACCOUNT_HASH}/{image_id}/{variant}"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Cloudflare Images Documentation](https://developers.cloudflare.com/images/)
|
||||
- [Direct Creator Upload](https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload/)
|
||||
- [Image Variants](https://developers.cloudflare.com/images/cloudflare-images/transform/flexible-variants/)
|
||||
Reference in New Issue
Block a user