## 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>
144 lines
4.8 KiB
Rust
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");
|
|
}
|
|
} |