feat: frontend MVP — auth, timeline, upload, albums, admin, image viewer

Backend:
- user roles (DB + JWT + first-user-is-admin)
- volume-aware file resolver (multi-volume asset serving)
- directory scanner uses volume URI directly
- date-summary endpoint (capture date from EXIF)
- timeline ordered by capture date
- list endpoints: volumes, plugins, pipelines, library paths
- delete endpoints: volumes, library paths
- configurable upload body limit (MAX_UPLOAD_BYTES)

Frontend:
- auth: login/register, token refresh, role-based admin gate
- timeline: date-grouped grid, infinite scroll, date scrubber
- image viewer: fullscreen zoom/pan/pinch, metadata sidebar
- upload: drag-drop, sequential upload, progress tracking
- albums: create, add/remove photos, asset picker dialog
- admin: storage (import library), jobs (pagination, error details),
  plugins (list + toggle), pipelines, sidecars, duplicates
- multi-select mode with add-to-album action
- TanStack Query for all data fetching
This commit is contained in:
2026-06-01 01:35:43 +02:00
parent 49f77a78b9
commit 957737ac9b
101 changed files with 4679 additions and 109 deletions

View File

@@ -32,6 +32,10 @@ pub trait AssetRepository: Send + Sync {
owner_id: &SystemId,
filters: &AssetFilters,
) -> Result<u64, DomainError>;
async fn date_summary(
&self,
owner_id: &SystemId,
) -> Result<Vec<(chrono::NaiveDate, u64)>, DomainError>;
async fn save(&self, asset: &Asset) -> Result<(), DomainError>;
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
}

View File

@@ -126,6 +126,7 @@ pub struct User {
pub username: String,
pub email: Email,
pub password_hash: PasswordHash,
pub role: String,
pub created_at: DateTime<Utc>,
}
@@ -136,9 +137,14 @@ impl User {
username: username.into(),
email,
password_hash,
role: "user".to_string(),
created_at: Utc::now(),
}
}
pub fn is_admin(&self) -> bool {
self.role == "admin"
}
}
// --- RefreshToken ---

View File

@@ -12,6 +12,7 @@ pub trait UserRepository: Send + Sync {
async fn find_by_username(&self, username: &str) -> Result<Option<User>, DomainError>;
async fn save(&self, user: &User) -> Result<(), DomainError>;
async fn delete(&self, id: &SystemId) -> Result<(), DomainError>;
async fn count(&self) -> Result<u64, DomainError>;
}
// --- RoleRepository ---

View File

@@ -34,6 +34,7 @@ pub trait JobBatchRepository: Send + Sync {
#[async_trait]
pub trait PluginRepository: Send + Sync {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<Plugin>, DomainError>;
async fn find_all(&self) -> Result<Vec<Plugin>, DomainError>;
async fn find_enabled(&self) -> Result<Vec<Plugin>, DomainError>;
async fn save(&self, plugin: &Plugin) -> Result<(), DomainError>;
}
@@ -43,6 +44,7 @@ pub trait PluginRepository: Send + Sync {
#[async_trait]
pub trait PipelineRepository: Send + Sync {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<ProcessingPipeline>, DomainError>;
async fn find_all(&self) -> Result<Vec<ProcessingPipeline>, DomainError>;
async fn find_by_trigger(&self, event: &str) -> Result<Vec<ProcessingPipeline>, DomainError>;
async fn save(&self, pipeline: &ProcessingPipeline) -> Result<(), DomainError>;
}

View File

@@ -23,6 +23,7 @@ pub trait StorageVolumeRepository: Send + Sync {
#[async_trait]
pub trait LibraryPathRepository: Send + Sync {
async fn find_by_id(&self, id: &SystemId) -> Result<Option<LibraryPath>, DomainError>;
async fn find_all(&self) -> Result<Vec<LibraryPath>, DomainError>;
async fn find_by_volume(&self, volume_id: &SystemId) -> Result<Vec<LibraryPath>, DomainError>;
async fn find_ingest_destinations(
&self,
@@ -84,6 +85,23 @@ pub trait IngestTransaction: Send + Sync {
async fn record_usage(&self, entry: &UsageLedgerEntry) -> Result<(), DomainError>;
}
// --- VolumeFileResolver ---
#[async_trait]
pub trait VolumeFileResolver: Send + Sync {
async fn open_by_volume(
&self,
volume_id: &SystemId,
relative_path: &str,
) -> Result<(DataStream, u64), DomainError>;
async fn read_by_volume(
&self,
volume_id: &SystemId,
relative_path: &str,
) -> Result<Bytes, DomainError>;
}
// --- FileStoragePort ---
#[derive(Debug, Clone)]