diff --git a/client/src/components/Memories/MemoriesPanel.tsx b/client/src/components/Memories/MemoriesPanel.tsx
index 74a72a0..2bc4f7d 100644
--- a/client/src/components/Memories/MemoriesPanel.tsx
+++ b/client/src/components/Memories/MemoriesPanel.tsx
@@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
-import { Camera, Plus, Share2, EyeOff, Eye, X, Check, Search, ArrowUpDown, MapPin, Filter } from 'lucide-react'
+import { Camera, Plus, Share2, EyeOff, Eye, X, Check, Search, ArrowUpDown, MapPin, Filter, Link2, RefreshCw, Unlink, FolderOpen } from 'lucide-react'
import apiClient from '../../api/client'
import { useAuthStore } from '../../store/authStore'
import { useTranslation } from '../../i18n'
@@ -52,6 +52,59 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
const [sortAsc, setSortAsc] = useState(true)
const [locationFilter, setLocationFilter] = useState('')
+ // Album linking
+ const [showAlbumPicker, setShowAlbumPicker] = useState(false)
+ const [albums, setAlbums] = useState<{ id: string; albumName: string; assetCount: number }[]>([])
+ const [albumsLoading, setAlbumsLoading] = useState(false)
+ const [albumLinks, setAlbumLinks] = useState<{ id: number; immich_album_id: string; album_name: string; user_id: number; username: string; sync_enabled: number; last_synced_at: string | null }[]>([])
+ const [syncing, setSyncing] = useState(null)
+
+ const loadAlbumLinks = async () => {
+ try {
+ const res = await apiClient.get(`/integrations/immich/trips/${tripId}/album-links`)
+ setAlbumLinks(res.data.links || [])
+ } catch { setAlbumLinks([]) }
+ }
+
+ const openAlbumPicker = async () => {
+ setShowAlbumPicker(true)
+ setAlbumsLoading(true)
+ try {
+ const res = await apiClient.get('/integrations/immich/albums')
+ setAlbums(res.data.albums || [])
+ } catch { setAlbums([]) }
+ finally { setAlbumsLoading(false) }
+ }
+
+ const linkAlbum = async (albumId: string, albumName: string) => {
+ try {
+ await apiClient.post(`/integrations/immich/trips/${tripId}/album-links`, { album_id: albumId, album_name: albumName })
+ setShowAlbumPicker(false)
+ await loadAlbumLinks()
+ // Auto-sync after linking
+ const linksRes = await apiClient.get(`/integrations/immich/trips/${tripId}/album-links`)
+ const newLink = (linksRes.data.links || []).find((l: any) => l.immich_album_id === albumId)
+ if (newLink) await syncAlbum(newLink.id)
+ } catch {}
+ }
+
+ const unlinkAlbum = async (linkId: number) => {
+ try {
+ await apiClient.delete(`/integrations/immich/trips/${tripId}/album-links/${linkId}`)
+ loadAlbumLinks()
+ } catch {}
+ }
+
+ const syncAlbum = async (linkId: number) => {
+ setSyncing(linkId)
+ try {
+ await apiClient.post(`/integrations/immich/trips/${tripId}/album-links/${linkId}/sync`)
+ await loadAlbumLinks()
+ await loadPhotos()
+ } catch {}
+ finally { setSyncing(null) }
+ }
+
// Lightbox
const [lightboxId, setLightboxId] = useState(null)
const [lightboxUserId, setLightboxUserId] = useState(null)
@@ -89,6 +142,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
setConnected(false)
}
await loadPhotos()
+ await loadAlbumLinks()
setLoading(false)
}
@@ -224,6 +278,72 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
// ── Photo Picker Modal ────────────────────────────────────────────────────
+ // ── Album Picker Modal ──────────────────────────────────────────────────
+
+ if (showAlbumPicker) {
+ const linkedIds = new Set(albumLinks.map(l => l.immich_album_id))
+ return (
+
+
+
+
+ {t('memories.selectAlbum')}
+
+
+
+
+
+ {albumsLoading ? (
+
+ ) : albums.length === 0 ? (
+
+ {t('memories.noAlbums')}
+
+ ) : (
+
+ {albums.map(album => {
+ const isLinked = linkedIds.has(album.id)
+ return (
+
+ )
+ })}
+
+ )}
+
+
+ )
+ }
+
+ // ── Photo Picker Modal ────────────────────────────────────────────────────
+
if (showPicker) {
const alreadyAdded = new Set(tripPhotos.filter(p => p.user_id === currentUser?.id).map(p => p.immich_asset_id))
@@ -404,16 +524,52 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
{connected && (
-
+
+
+
+
)}
+
+ {/* Linked Albums */}
+ {albumLinks.length > 0 && (
+
+ {albumLinks.map(link => (
+
+
+ {link.album_name}
+ {link.username !== currentUser?.username && ({link.username})}
+
+ {link.user_id === currentUser?.id && (
+
+ )}
+
+ ))}
+
+ )}
{/* Filter & Sort bar */}
diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts
index 1b08bf8..e0587a9 100644
--- a/client/src/i18n/translations/ar.ts
+++ b/client/src/i18n/translations/ar.ts
@@ -1350,6 +1350,12 @@ const ar: Record = {
'memories.newest': 'الأحدث أولاً',
'memories.allLocations': 'جميع المواقع',
'memories.addPhotos': 'إضافة صور',
+ 'memories.linkAlbum': 'ربط ألبوم',
+ 'memories.selectAlbum': 'اختيار ألبوم Immich',
+ 'memories.noAlbums': 'لم يتم العثور على ألبومات',
+ 'memories.syncAlbum': 'مزامنة الألبوم',
+ 'memories.unlinkAlbum': 'إلغاء الربط',
+ 'memories.photos': 'صور',
'memories.selectPhotos': 'اختيار صور من Immich',
'memories.selectHint': 'انقر على الصور لتحديدها.',
'memories.selected': 'محدد',
diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts
index 054ae5f..9ea1d24 100644
--- a/client/src/i18n/translations/br.ts
+++ b/client/src/i18n/translations/br.ts
@@ -1398,6 +1398,12 @@ const br: Record = {
'memories.connectionError': 'Não foi possível conectar ao Immich',
'memories.saved': 'Configurações do Immich salvas',
'memories.addPhotos': 'Adicionar fotos',
+ 'memories.linkAlbum': 'Vincular álbum',
+ 'memories.selectAlbum': 'Selecionar álbum do Immich',
+ 'memories.noAlbums': 'Nenhum álbum encontrado',
+ 'memories.syncAlbum': 'Sincronizar álbum',
+ 'memories.unlinkAlbum': 'Desvincular',
+ 'memories.photos': 'fotos',
'memories.selectPhotos': 'Selecionar fotos do Immich',
'memories.selectHint': 'Toque nas fotos para selecioná-las.',
'memories.selected': 'selecionadas',
diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts
index 26f0c33..09c625b 100644
--- a/client/src/i18n/translations/cs.ts
+++ b/client/src/i18n/translations/cs.ts
@@ -1347,6 +1347,12 @@ const cs: Record = {
'memories.connectionError': 'Nepodařilo se připojit k Immich',
'memories.saved': 'Nastavení Immich uloženo',
'memories.addPhotos': 'Přidat fotky',
+ 'memories.linkAlbum': 'Propojit album',
+ 'memories.selectAlbum': 'Vybrat album z Immich',
+ 'memories.noAlbums': 'Žádná alba nenalezena',
+ 'memories.syncAlbum': 'Synchronizovat album',
+ 'memories.unlinkAlbum': 'Odpojit',
+ 'memories.photos': 'fotek',
'memories.selectPhotos': 'Vybrat fotky z Immich',
'memories.selectHint': 'Klepněte na fotky pro jejich výběr.',
'memories.selected': 'vybráno',
diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts
index 0c32841..f7ba5da 100644
--- a/client/src/i18n/translations/de.ts
+++ b/client/src/i18n/translations/de.ts
@@ -1344,6 +1344,12 @@ const de: Record = {
'memories.connectionError': 'Verbindung zu Immich fehlgeschlagen',
'memories.saved': 'Immich-Einstellungen gespeichert',
'memories.addPhotos': 'Fotos hinzufügen',
+ 'memories.linkAlbum': 'Album verknüpfen',
+ 'memories.selectAlbum': 'Immich-Album auswählen',
+ 'memories.noAlbums': 'Keine Alben gefunden',
+ 'memories.syncAlbum': 'Album synchronisieren',
+ 'memories.unlinkAlbum': 'Album trennen',
+ 'memories.photos': 'Fotos',
'memories.selectPhotos': 'Fotos aus Immich auswählen',
'memories.selectHint': 'Tippe auf Fotos um sie auszuwählen.',
'memories.selected': 'ausgewählt',
diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts
index d20130d..3a6cef8 100644
--- a/client/src/i18n/translations/en.ts
+++ b/client/src/i18n/translations/en.ts
@@ -1341,6 +1341,12 @@ const en: Record = {
'memories.connectionError': 'Could not connect to Immich',
'memories.saved': 'Immich settings saved',
'memories.addPhotos': 'Add photos',
+ 'memories.linkAlbum': 'Link Album',
+ 'memories.selectAlbum': 'Select Immich Album',
+ 'memories.noAlbums': 'No albums found',
+ 'memories.syncAlbum': 'Sync album',
+ 'memories.unlinkAlbum': 'Unlink album',
+ 'memories.photos': 'photos',
'memories.selectPhotos': 'Select photos from Immich',
'memories.selectHint': 'Tap photos to select them.',
'memories.selected': 'selected',
diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts
index 9c0dedc..82d2bd6 100644
--- a/client/src/i18n/translations/es.ts
+++ b/client/src/i18n/translations/es.ts
@@ -1300,6 +1300,12 @@ const es: Record = {
'memories.newest': 'Más recientes',
'memories.allLocations': 'Todas las ubicaciones',
'memories.addPhotos': 'Añadir fotos',
+ 'memories.linkAlbum': 'Vincular álbum',
+ 'memories.selectAlbum': 'Seleccionar álbum de Immich',
+ 'memories.noAlbums': 'No se encontraron álbumes',
+ 'memories.syncAlbum': 'Sincronizar álbum',
+ 'memories.unlinkAlbum': 'Desvincular',
+ 'memories.photos': 'fotos',
'memories.selectPhotos': 'Seleccionar fotos de Immich',
'memories.selectHint': 'Toca las fotos para seleccionarlas.',
'memories.selected': 'seleccionado(s)',
diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts
index d0caa3a..2abd843 100644
--- a/client/src/i18n/translations/fr.ts
+++ b/client/src/i18n/translations/fr.ts
@@ -1346,6 +1346,12 @@ const fr: Record = {
'memories.newest': 'Plus récentes',
'memories.allLocations': 'Tous les lieux',
'memories.addPhotos': 'Ajouter des photos',
+ 'memories.linkAlbum': 'Lier un album',
+ 'memories.selectAlbum': 'Choisir un album Immich',
+ 'memories.noAlbums': 'Aucun album trouvé',
+ 'memories.syncAlbum': 'Synchroniser',
+ 'memories.unlinkAlbum': 'Délier',
+ 'memories.photos': 'photos',
'memories.selectPhotos': 'Sélectionner des photos depuis Immich',
'memories.selectHint': 'Appuyez sur les photos pour les sélectionner.',
'memories.selected': 'sélectionné(s)',
diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts
index 34589ce..9f0c608 100644
--- a/client/src/i18n/translations/hu.ts
+++ b/client/src/i18n/translations/hu.ts
@@ -1414,6 +1414,12 @@ const hu: Record = {
'memories.connectionError': 'Nem sikerült csatlakozni az Immichhez',
'memories.saved': 'Immich beállítások mentve',
'memories.addPhotos': 'Fotók hozzáadása',
+ 'memories.linkAlbum': 'Album csatolása',
+ 'memories.selectAlbum': 'Immich album kiválasztása',
+ 'memories.noAlbums': 'Nem található album',
+ 'memories.syncAlbum': 'Album szinkronizálása',
+ 'memories.unlinkAlbum': 'Leválasztás',
+ 'memories.photos': 'fotó',
'memories.selectPhotos': 'Fotók kiválasztása az Immichből',
'memories.selectHint': 'Koppints a fotókra a kijelölésükhöz.',
'memories.selected': 'kijelölve',
diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts
index 377a2dd..5c3ee54 100644
--- a/client/src/i18n/translations/it.ts
+++ b/client/src/i18n/translations/it.ts
@@ -1344,6 +1344,12 @@ const it: Record = {
'memories.connectionError': 'Impossibile connettersi a Immich',
'memories.saved': 'Impostazioni Immich salvate',
'memories.addPhotos': 'Aggiungi foto',
+ 'memories.linkAlbum': 'Collega album',
+ 'memories.selectAlbum': 'Seleziona album Immich',
+ 'memories.noAlbums': 'Nessun album trovato',
+ 'memories.syncAlbum': 'Sincronizza album',
+ 'memories.unlinkAlbum': 'Scollega',
+ 'memories.photos': 'foto',
'memories.selectPhotos': 'Seleziona foto da Immich',
'memories.selectHint': 'Tocca le foto per selezionarle.',
'memories.selected': 'selezionate',
diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts
index eb22e5b..b47f198 100644
--- a/client/src/i18n/translations/nl.ts
+++ b/client/src/i18n/translations/nl.ts
@@ -1346,6 +1346,12 @@ const nl: Record = {
'memories.newest': 'Nieuwste eerst',
'memories.allLocations': 'Alle locaties',
'memories.addPhotos': 'Foto\'s toevoegen',
+ 'memories.linkAlbum': 'Album koppelen',
+ 'memories.selectAlbum': 'Immich-album selecteren',
+ 'memories.noAlbums': 'Geen albums gevonden',
+ 'memories.syncAlbum': 'Album synchroniseren',
+ 'memories.unlinkAlbum': 'Ontkoppelen',
+ 'memories.photos': 'fotos',
'memories.selectPhotos': 'Selecteer foto\'s uit Immich',
'memories.selectHint': 'Tik op foto\'s om ze te selecteren.',
'memories.selected': 'geselecteerd',
diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts
index 03c843a..9f74102 100644
--- a/client/src/i18n/translations/ru.ts
+++ b/client/src/i18n/translations/ru.ts
@@ -1346,6 +1346,12 @@ const ru: Record = {
'memories.newest': 'Сначала новые',
'memories.allLocations': 'Все места',
'memories.addPhotos': 'Добавить фото',
+ 'memories.linkAlbum': 'Привязать альбом',
+ 'memories.selectAlbum': 'Выбрать альбом Immich',
+ 'memories.noAlbums': 'Альбомы не найдены',
+ 'memories.syncAlbum': 'Синхронизировать',
+ 'memories.unlinkAlbum': 'Отвязать',
+ 'memories.photos': 'фото',
'memories.selectPhotos': 'Выбрать фото из Immich',
'memories.selectHint': 'Нажмите на фото, чтобы выбрать их.',
'memories.selected': 'выбрано',
diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts
index 156dec5..b376baf 100644
--- a/client/src/i18n/translations/zh.ts
+++ b/client/src/i18n/translations/zh.ts
@@ -1346,6 +1346,12 @@ const zh: Record = {
'memories.newest': '最新优先',
'memories.allLocations': '所有地点',
'memories.addPhotos': '添加照片',
+ 'memories.linkAlbum': '关联相册',
+ 'memories.selectAlbum': '选择 Immich 相册',
+ 'memories.noAlbums': '未找到相册',
+ 'memories.syncAlbum': '同步相册',
+ 'memories.unlinkAlbum': '取消关联',
+ 'memories.photos': '张照片',
'memories.selectPhotos': '从 Immich 选择照片',
'memories.selectHint': '点击照片以选择。',
'memories.selected': '已选择',
diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts
index e6f2929..9a0106a 100644
--- a/server/src/db/migrations.ts
+++ b/server/src/db/migrations.ts
@@ -439,6 +439,22 @@ function runMigrations(db: Database.Database): void {
() => {
try { db.exec('ALTER TABLE budget_items ADD COLUMN expense_date TEXT DEFAULT NULL'); } catch {}
},
+ () => {
+ db.exec(`
+ CREATE TABLE IF NOT EXISTS trip_album_links (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ trip_id INTEGER NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ immich_album_id TEXT NOT NULL,
+ album_name TEXT NOT NULL DEFAULT '',
+ sync_enabled INTEGER NOT NULL DEFAULT 1,
+ last_synced_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(trip_id, user_id, immich_album_id)
+ );
+ CREATE INDEX IF NOT EXISTS idx_trip_album_links_trip ON trip_album_links(trip_id);
+ `);
+ },
];
if (currentVersion < migrations.length) {
diff --git a/server/src/routes/immich.ts b/server/src/routes/immich.ts
index ef3891b..8421df1 100644
--- a/server/src/routes/immich.ts
+++ b/server/src/routes/immich.ts
@@ -268,8 +268,9 @@ router.get('/assets/:assetId/thumbnail', authFromQuery, async (req: Request, res
const { assetId } = req.params;
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
- // Only allow accessing own Immich credentials — prevent leaking other users' API keys
- const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any;
+ // Use photo owner's Immich credentials if userId is provided (for shared photos)
+ const targetUserId = req.query.userId ? Number(req.query.userId) : authReq.user.id;
+ const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(targetUserId) as any;
if (!user?.immich_url || !user?.immich_api_key) return res.status(404).send('Not found');
try {
@@ -292,8 +293,9 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res:
const { assetId } = req.params;
if (!isValidAssetId(assetId)) return res.status(400).send('Invalid asset ID');
- // Only allow accessing own Immich credentials — prevent leaking other users' API keys
- const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any;
+ // Use photo owner's Immich credentials if userId is provided (for shared photos)
+ const targetUserId = req.query.userId ? Number(req.query.userId) : authReq.user.id;
+ const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(targetUserId) as any;
if (!user?.immich_url || !user?.immich_api_key) return res.status(404).send('Not found');
try {
@@ -311,4 +313,110 @@ router.get('/assets/:assetId/original', authFromQuery, async (req: Request, res:
}
});
+// ── Album Linking ──────────────────────────────────────────────────────────
+
+// List user's Immich albums
+router.get('/albums', authenticate, async (req: Request, res: Response) => {
+ const authReq = req as AuthRequest;
+ const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any;
+ if (!user?.immich_url || !user?.immich_api_key) return res.status(400).json({ error: 'Immich not configured' });
+
+ try {
+ const resp = await fetch(`${user.immich_url}/api/albums`, {
+ headers: { 'x-api-key': user.immich_api_key, 'Accept': 'application/json' },
+ signal: AbortSignal.timeout(10000),
+ });
+ if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch albums' });
+ const albums = (await resp.json() as any[]).map((a: any) => ({
+ id: a.id,
+ albumName: a.albumName,
+ assetCount: a.assetCount || 0,
+ startDate: a.startDate,
+ endDate: a.endDate,
+ albumThumbnailAssetId: a.albumThumbnailAssetId,
+ }));
+ res.json({ albums });
+ } catch {
+ res.status(502).json({ error: 'Could not reach Immich' });
+ }
+});
+
+// Get album links for a trip
+router.get('/trips/:tripId/album-links', authenticate, (req: Request, res: Response) => {
+ const links = db.prepare(`
+ SELECT tal.*, u.username
+ FROM trip_album_links tal
+ JOIN users u ON tal.user_id = u.id
+ WHERE tal.trip_id = ?
+ ORDER BY tal.created_at ASC
+ `).all(req.params.tripId);
+ res.json({ links });
+});
+
+// Link an album to a trip
+router.post('/trips/:tripId/album-links', authenticate, async (req: Request, res: Response) => {
+ const authReq = req as AuthRequest;
+ const { tripId } = req.params;
+ const { album_id, album_name } = req.body;
+ if (!album_id) return res.status(400).json({ error: 'album_id required' });
+
+ try {
+ db.prepare(
+ 'INSERT OR IGNORE INTO trip_album_links (trip_id, user_id, immich_album_id, album_name) VALUES (?, ?, ?, ?)'
+ ).run(tripId, authReq.user.id, album_id, album_name || '');
+ res.json({ success: true });
+ } catch (err: any) {
+ res.status(400).json({ error: 'Album already linked' });
+ }
+});
+
+// Remove album link
+router.delete('/trips/:tripId/album-links/:linkId', authenticate, (req: Request, res: Response) => {
+ const authReq = req as AuthRequest;
+ db.prepare('DELETE FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?')
+ .run(req.params.linkId, req.params.tripId, authReq.user.id);
+ res.json({ success: true });
+});
+
+// Sync album — fetch all assets from Immich album and add missing ones to trip
+router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
+ const authReq = req as AuthRequest;
+ const { tripId, linkId } = req.params;
+
+ const link = db.prepare('SELECT * FROM trip_album_links WHERE id = ? AND trip_id = ? AND user_id = ?')
+ .get(linkId, tripId, authReq.user.id) as any;
+ if (!link) return res.status(404).json({ error: 'Album link not found' });
+
+ const user = db.prepare('SELECT immich_url, immich_api_key FROM users WHERE id = ?').get(authReq.user.id) as any;
+ if (!user?.immich_url || !user?.immich_api_key) return res.status(400).json({ error: 'Immich not configured' });
+
+ try {
+ const resp = await fetch(`${user.immich_url}/api/albums/${link.immich_album_id}`, {
+ headers: { 'x-api-key': user.immich_api_key, 'Accept': 'application/json' },
+ signal: AbortSignal.timeout(15000),
+ });
+ if (!resp.ok) return res.status(resp.status).json({ error: 'Failed to fetch album' });
+ const albumData = await resp.json() as { assets?: any[] };
+ const assets = (albumData.assets || []).filter((a: any) => a.type === 'IMAGE');
+
+ const insert = db.prepare(
+ 'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, immich_asset_id, shared) VALUES (?, ?, ?, 1)'
+ );
+ let added = 0;
+ for (const asset of assets) {
+ const r = insert.run(tripId, authReq.user.id, asset.id);
+ if (r.changes > 0) added++;
+ }
+
+ db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
+
+ res.json({ success: true, added, total: assets.length });
+ if (added > 0) {
+ broadcast(tripId, 'memories:updated', { userId: authReq.user.id }, req.headers['x-socket-id'] as string);
+ }
+ } catch {
+ res.status(502).json({ error: 'Could not reach Immich' });
+ }
+});
+
export default router;