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:
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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 ---
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user