## Major Achievements ✅ ### Story 1.14: 前端事件画廊页面 - Gallery Page Implementation - ✅ Protected /gallery route with authentication redirect - ✅ Infinite scroll with React Query + Intersection Observer - ✅ Responsive event cards with thumbnail, date, location - ✅ Loading states, empty states, error handling - ✅ Dark theme UI consistent with design system ### Full-Stack Integration Testing Framework - ✅ Docker-based test environment (PostgreSQL + LocalStack) - ✅ E2E tests with Playwright (authentication, gallery workflows) - ✅ API integration tests covering complete user journeys - ✅ Automated test data generation and cleanup - ✅ Performance and concurrency testing ### Technical Stack Validation - ✅ Next.js 15 + React Query + TypeScript frontend - ✅ NestJS + TypeORM + PostgreSQL backend - ✅ AWS S3/SQS integration (LocalStack for testing) - ✅ JWT authentication with secure token management - ✅ Complete data pipeline: Edge → Backend → Processing → Gallery ## Files Added/Modified ### Frontend Implementation - src/app/gallery/page.tsx - Main gallery page with auth protection - src/services/events.ts - API client for events with pagination - src/hooks/use-events.ts - React Query hooks for infinite scroll - src/components/gallery/ - Modular UI components (EventCard, GalleryGrid, States) - src/contexts/query-provider.tsx - React Query configuration ### Testing Infrastructure - docker-compose.test.yml - Complete test environment setup - test-setup.sh - One-command test environment initialization - test-data/seed-test-data.js - Automated test data generation - e2e/gallery.spec.ts - Comprehensive E2E gallery tests - test/integration.e2e-spec.ts - Full-stack workflow validation - TESTING.md - Complete testing guide and documentation ### Project Configuration - package.json (root) - Monorepo scripts and workspace management - playwright.config.ts - E2E testing configuration - .env.test - Test environment variables - README.md - Project documentation ## Test Results 📊 - ✅ Unit Tests: 10/10 passing (Frontend components) - ✅ Integration Tests: Full workflow validation - ✅ E2E Tests: Complete user journey coverage - ✅ Lint: No warnings or errors - ✅ Build: Production ready (11.7kB gallery page) ## Milestone: Epic 1 "First Light" Achieved 🚀 The complete data flow is now validated: 1. User Authentication ✅ 2. Device Registration ✅ 3. Event Upload Pipeline ✅ 4. Background Processing ✅ 5. Gallery Display ✅ This establishes the foundation for all future development. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// RawEvent represents a raw event from the database
|
|
type RawEvent struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
DeviceID uuid.UUID `json:"device_id" db:"device_id"`
|
|
UserProfileID uuid.UUID `json:"user_profile_id" db:"user_profile_id"`
|
|
FilePath string `json:"file_path" db:"file_path"`
|
|
FileSize *int64 `json:"file_size" db:"file_size"`
|
|
FileType *string `json:"file_type" db:"file_type"`
|
|
OriginalFilename *string `json:"original_filename" db:"original_filename"`
|
|
EventType string `json:"event_type" db:"event_type"`
|
|
EventTimestamp time.Time `json:"event_timestamp" db:"event_timestamp"`
|
|
Metadata json.RawMessage `json:"metadata" db:"metadata"`
|
|
ProcessingStatus string `json:"processing_status" db:"processing_status"`
|
|
SqsMessageID *string `json:"sqs_message_id" db:"sqs_message_id"`
|
|
ProcessedAt *time.Time `json:"processed_at" db:"processed_at"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ValidatedEvent represents a validated event that will be stored in the database
|
|
type ValidatedEvent struct {
|
|
ID uuid.UUID `json:"id" db:"id"`
|
|
RawEventID uuid.UUID `json:"raw_event_id" db:"raw_event_id"`
|
|
DeviceID uuid.UUID `json:"device_id" db:"device_id"`
|
|
UserProfileID uuid.UUID `json:"user_profile_id" db:"user_profile_id"`
|
|
MediaURL string `json:"media_url" db:"media_url"`
|
|
FileSize *int64 `json:"file_size" db:"file_size"`
|
|
FileType *string `json:"file_type" db:"file_type"`
|
|
OriginalFilename *string `json:"original_filename" db:"original_filename"`
|
|
EventType string `json:"event_type" db:"event_type"`
|
|
EventTimestamp time.Time `json:"event_timestamp" db:"event_timestamp"`
|
|
Metadata json.RawMessage `json:"metadata" db:"metadata"`
|
|
ValidationScore *float64 `json:"validation_score" db:"validation_score"`
|
|
ValidationDetails json.RawMessage `json:"validation_details" db:"validation_details"`
|
|
IsValid bool `json:"is_valid" db:"is_valid"`
|
|
ValidationAlgorithm *string `json:"validation_algorithm" db:"validation_algorithm"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
|
|
// ProcessingStatus constants for raw events
|
|
const (
|
|
ProcessingStatusPending = "pending"
|
|
ProcessingStatusProcessing = "processing"
|
|
ProcessingStatusCompleted = "completed"
|
|
ProcessingStatusFailed = "failed"
|
|
)
|
|
|
|
// EventType constants
|
|
const (
|
|
EventTypeMotion = "motion"
|
|
EventTypeAlert = "alert"
|
|
EventTypeMeteor = "meteor"
|
|
)
|
|
|
|
// ValidationResult represents the result of event validation
|
|
type ValidationResult struct {
|
|
IsValid bool `json:"is_valid"`
|
|
Score float64 `json:"score"`
|
|
Algorithm string `json:"algorithm"`
|
|
Details json.RawMessage `json:"details"`
|
|
Reason string `json:"reason,omitempty"`
|
|
ProcessedAt time.Time `json:"processed_at"`
|
|
}
|
|
|
|
// CreateValidatedEventFromRaw creates a ValidatedEvent from a RawEvent
|
|
func CreateValidatedEventFromRaw(rawEvent *RawEvent, validationResult *ValidationResult) *ValidatedEvent {
|
|
// Generate media URL from file path (this would typically involve S3 signed URLs or CloudFront)
|
|
mediaURL := generateMediaURL(rawEvent.FilePath)
|
|
|
|
// Serialize validation details
|
|
validationDetails, err := json.Marshal(validationResult.Details)
|
|
if err != nil {
|
|
validationDetails = json.RawMessage("{}")
|
|
}
|
|
|
|
return &ValidatedEvent{
|
|
ID: uuid.New(),
|
|
RawEventID: rawEvent.ID,
|
|
DeviceID: rawEvent.DeviceID,
|
|
UserProfileID: rawEvent.UserProfileID,
|
|
MediaURL: mediaURL,
|
|
FileSize: rawEvent.FileSize,
|
|
FileType: rawEvent.FileType,
|
|
OriginalFilename: rawEvent.OriginalFilename,
|
|
EventType: rawEvent.EventType,
|
|
EventTimestamp: rawEvent.EventTimestamp,
|
|
Metadata: rawEvent.Metadata,
|
|
ValidationScore: &validationResult.Score,
|
|
ValidationDetails: validationDetails,
|
|
IsValid: validationResult.IsValid,
|
|
ValidationAlgorithm: &validationResult.Algorithm,
|
|
CreatedAt: time.Now().UTC(),
|
|
UpdatedAt: time.Now().UTC(),
|
|
}
|
|
}
|
|
|
|
// generateMediaURL creates a publicly accessible URL for the media file
|
|
// In a real implementation, this would create S3 signed URLs or CloudFront URLs
|
|
func generateMediaURL(filePath string) string {
|
|
// For MVP, we'll use a simple URL format
|
|
// In production, this should use AWS S3 signed URLs or CloudFront
|
|
baseURL := "https://meteor-media.example.com"
|
|
return baseURL + "/" + filePath
|
|
} |