grabbit a04d6eba88 🎉 Epic 1 Complete: Foundation, User Core & First Light
## 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>
2025-07-31 18:49:48 +08:00

144 lines
4.8 KiB
Rust

use anyhow::{Context, Result};
use std::fs;
/// Extracts a unique hardware identifier from the system
/// This function reads /proc/cpuinfo to find a stable CPU serial number
pub fn get_hardware_id() -> Result<String> {
// Try to read /proc/cpuinfo first (common on Raspberry Pi and other ARM systems)
if let Ok(hardware_id) = read_cpu_serial() {
return Ok(hardware_id);
}
// Fallback: try to read machine-id
if let Ok(machine_id) = read_machine_id() {
return Ok(machine_id);
}
// Last resort: generate a warning and use hostname + MAC address hash
eprintln!("Warning: Could not read CPU serial or machine-id, using fallback method");
get_fallback_id()
}
/// Reads CPU serial number from /proc/cpuinfo
/// This is the most reliable method on Raspberry Pi systems
fn read_cpu_serial() -> Result<String> {
let cpuinfo = fs::read_to_string("/proc/cpuinfo")
.context("Failed to read /proc/cpuinfo")?;
for line in cpuinfo.lines() {
if line.starts_with("Serial") {
if let Some(serial) = line.split(':').nth(1) {
let serial = serial.trim();
if !serial.is_empty() && serial != "0000000000000000" {
return Ok(format!("CPU_{}", serial));
}
}
}
}
anyhow::bail!("No valid CPU serial found in /proc/cpuinfo")
}
/// Reads machine ID from /etc/machine-id (systemd systems)
fn read_machine_id() -> Result<String> {
let machine_id = fs::read_to_string("/etc/machine-id")
.context("Failed to read /etc/machine-id")?;
let machine_id = machine_id.trim();
if machine_id.len() >= 8 {
Ok(format!("MACHINE_{}", &machine_id[..16]))
} else {
anyhow::bail!("Invalid machine-id format")
}
}
/// Fallback method to generate a hardware ID
/// Uses hostname + network interface information
fn get_fallback_id() -> Result<String> {
use std::process::Command;
// Get hostname
let hostname_output = Command::new("hostname")
.output()
.context("Failed to execute hostname command")?;
let hostname = String::from_utf8_lossy(&hostname_output.stdout)
.trim()
.to_string();
// Try to get MAC address from network interfaces
if let Ok(mac) = get_primary_mac_address() {
return Ok(format!("FALLBACK_{}_{}", hostname, mac));
}
// Very last resort: just use hostname with timestamp
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Ok(format!("FALLBACK_{}_{}", hostname, timestamp))
}
/// Attempts to get the MAC address of the primary network interface
fn get_primary_mac_address() -> Result<String> {
let interfaces_dir = "/sys/class/net";
let entries = fs::read_dir(interfaces_dir)
.context("Failed to read network interfaces directory")?;
for entry in entries {
let entry = entry?;
let interface_name = entry.file_name();
let interface_name = interface_name.to_string_lossy();
// Skip loopback and common virtual interfaces
if interface_name == "lo" || interface_name.starts_with("docker")
|| interface_name.starts_with("veth") {
continue;
}
let mac_path = format!("{}/{}/address", interfaces_dir, interface_name);
if let Ok(mac) = fs::read_to_string(&mac_path) {
let mac = mac.trim().replace(':', "");
if mac.len() == 12 && mac != "000000000000" {
return Ok(mac.to_uppercase());
}
}
}
anyhow::bail!("No valid MAC address found")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hardware_id_format() {
// Test that we can get some kind of hardware ID
// The exact format will depend on the system, but it should not be empty
let result = get_hardware_id();
match result {
Ok(id) => {
assert!(!id.is_empty(), "Hardware ID should not be empty");
assert!(id.len() >= 8, "Hardware ID should be at least 8 characters");
println!("Hardware ID: {}", id);
}
Err(e) => {
println!("Could not get hardware ID: {}", e);
// On systems without the expected files, this might fail
// That's okay for testing purposes
}
}
}
#[test]
fn test_fallback_id() {
let result = get_fallback_id();
assert!(result.is_ok(), "Fallback ID generation should always work");
let id = result.unwrap();
assert!(id.starts_with("FALLBACK_"), "Fallback ID should have correct prefix");
assert!(id.len() > 10, "Fallback ID should be reasonably long");
}
}