fix(photos): faster large-album loading and album cover lifecycle
Large albums opened slowly and flickered; the default album cover was also lost in the media-objects refactor. Large-album loading: - quantize signed object URLs within a window and send Cache-Control so previews are reused instead of re-fetched - publish photo.updated (not photo.created) when variants are ready, so clients update in place without reordering the grid - worker concurrency + newest-first variant priority; raise default batch Album cover: - first uploaded photo becomes the cover when none is set - clear the cover when the cover photo is trashed or moved out - "set as album cover" in the photo context menu and actions panel OpenSpec changes: fix-large-album-load, fix-album-cover
This commit is contained in:
@@ -48,16 +48,19 @@ type SSE struct {
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Interval time.Duration
|
||||
Batch int
|
||||
PendingTTL time.Duration
|
||||
Interval time.Duration
|
||||
Batch int
|
||||
PendingTTL time.Duration
|
||||
Concurrency int
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
Backend string
|
||||
FSRoot string
|
||||
URLSecret string
|
||||
URLTTL time.Duration
|
||||
Backend string
|
||||
FSRoot string
|
||||
URLSecret string
|
||||
URLTTL time.Duration
|
||||
URLSignWindow time.Duration
|
||||
CacheMaxAge time.Duration
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
@@ -103,6 +106,16 @@ func Load() (*Config, error) {
|
||||
return nil, fmt.Errorf("invalid OBJECT_URL_TTL: %w", err)
|
||||
}
|
||||
|
||||
urlSignWindow, err := time.ParseDuration(getEnv("OBJECT_URL_SIGN_WINDOW", "10m"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid OBJECT_URL_SIGN_WINDOW: %w", err)
|
||||
}
|
||||
|
||||
cacheMaxAge, err := time.ParseDuration(getEnv("OBJECT_CACHE_MAX_AGE", "1h"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid OBJECT_CACHE_MAX_AGE: %w", err)
|
||||
}
|
||||
|
||||
heartbeat, err := time.ParseDuration(getEnv("SSE_HEARTBEAT_INTERVAL", "25s"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SSE_HEARTBEAT_INTERVAL: %w", err)
|
||||
@@ -123,7 +136,7 @@ func Load() (*Config, error) {
|
||||
return nil, fmt.Errorf("invalid WORKER_INTERVAL: %w", err)
|
||||
}
|
||||
|
||||
workerBatch, err := strconv.Atoi(getEnv("WORKER_BATCH", "10"))
|
||||
workerBatch, err := strconv.Atoi(getEnv("WORKER_BATCH", "100"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid WORKER_BATCH: %w", err)
|
||||
}
|
||||
@@ -133,6 +146,11 @@ func Load() (*Config, error) {
|
||||
return nil, fmt.Errorf("invalid WORKER_PENDING_TTL: %w", err)
|
||||
}
|
||||
|
||||
workerConcurrency, err := strconv.Atoi(getEnv("WORKER_CONCURRENCY", "4"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid WORKER_CONCURRENCY: %w", err)
|
||||
}
|
||||
|
||||
logLevel := getEnv("LOG_LEVEL", "info")
|
||||
if _, err := logging.ParseLevel(logLevel); err != nil {
|
||||
return nil, err
|
||||
@@ -160,10 +178,12 @@ func Load() (*Config, error) {
|
||||
UsePathStyle: pathStyle,
|
||||
},
|
||||
Storage: Storage{
|
||||
Backend: getEnv("STORAGE_BACKEND", "fs"),
|
||||
FSRoot: getEnv("FS_ROOT", "./data"),
|
||||
URLSecret: getEnv("URL_SECRET", ""),
|
||||
URLTTL: urlTTL,
|
||||
Backend: getEnv("STORAGE_BACKEND", "fs"),
|
||||
FSRoot: getEnv("FS_ROOT", "./data"),
|
||||
URLSecret: getEnv("URL_SECRET", ""),
|
||||
URLTTL: urlTTL,
|
||||
URLSignWindow: urlSignWindow,
|
||||
CacheMaxAge: cacheMaxAge,
|
||||
},
|
||||
Database: Database{
|
||||
Backend: getEnv("DB_BACKEND", "sqlite"),
|
||||
@@ -182,9 +202,10 @@ func Load() (*Config, error) {
|
||||
ClientBuffer: bufSize,
|
||||
},
|
||||
Worker: Worker{
|
||||
Interval: workerInterval,
|
||||
Batch: workerBatch,
|
||||
PendingTTL: workerPendingTTL,
|
||||
Interval: workerInterval,
|
||||
Batch: workerBatch,
|
||||
PendingTTL: workerPendingTTL,
|
||||
Concurrency: workerConcurrency,
|
||||
},
|
||||
Log: Log{
|
||||
Level: logLevel,
|
||||
|
||||
Reference in New Issue
Block a user