Merge branch 'test' into dev

This commit is contained in:
Marek Maslowski
2026-04-04 19:27:16 +02:00
22 changed files with 2230 additions and 479 deletions
+183
View File
@@ -518,6 +518,189 @@ function runMigrations(db: Database.Database): void {
CREATE INDEX IF NOT EXISTS idx_notifications_recipient_created ON notifications(recipient_id, created_at DESC);
`);
},
() => {
// Normalize trip_photos to provider-based schema used by current routes
const tripPhotosExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'trip_photos'").get();
if (!tripPhotosExists) {
db.exec(`
CREATE TABLE trip_photos (
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,
asset_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'immich',
shared INTEGER NOT NULL DEFAULT 1,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(trip_id, user_id, asset_id, provider)
);
CREATE INDEX IF NOT EXISTS idx_trip_photos_trip ON trip_photos(trip_id);
`);
} else {
const columns = db.prepare("PRAGMA table_info('trip_photos')").all() as Array<{ name: string }>;
const names = new Set(columns.map(c => c.name));
const assetSource = names.has('asset_id') ? 'asset_id' : (names.has('immich_asset_id') ? 'immich_asset_id' : null);
if (assetSource) {
const providerExpr = names.has('provider')
? "CASE WHEN provider IS NULL OR provider = '' THEN 'immich' ELSE provider END"
: "'immich'";
const sharedExpr = names.has('shared') ? 'COALESCE(shared, 1)' : '1';
const addedAtExpr = names.has('added_at') ? 'COALESCE(added_at, CURRENT_TIMESTAMP)' : 'CURRENT_TIMESTAMP';
db.exec(`
CREATE TABLE trip_photos_new (
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,
asset_id TEXT NOT NULL,
provider TEXT NOT NULL DEFAULT 'immich',
shared INTEGER NOT NULL DEFAULT 1,
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(trip_id, user_id, asset_id, provider)
);
`);
db.exec(`
INSERT OR IGNORE INTO trip_photos_new (trip_id, user_id, asset_id, provider, shared, added_at)
SELECT trip_id, user_id, ${assetSource}, ${providerExpr}, ${sharedExpr}, ${addedAtExpr}
FROM trip_photos
WHERE ${assetSource} IS NOT NULL AND TRIM(${assetSource}) != ''
`);
db.exec('DROP TABLE trip_photos');
db.exec('ALTER TABLE trip_photos_new RENAME TO trip_photos');
db.exec('CREATE INDEX IF NOT EXISTS idx_trip_photos_trip ON trip_photos(trip_id)');
}
}
},
() => {
// Normalize trip_album_links to provider + album_id schema used by current routes
const linksExists = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'trip_album_links'").get();
if (!linksExists) {
db.exec(`
CREATE TABLE 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,
provider TEXT NOT NULL,
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, provider, album_id)
);
CREATE INDEX IF NOT EXISTS idx_trip_album_links_trip ON trip_album_links(trip_id);
`);
} else {
const columns = db.prepare("PRAGMA table_info('trip_album_links')").all() as Array<{ name: string }>;
const names = new Set(columns.map(c => c.name));
const albumIdSource = names.has('album_id') ? 'album_id' : (names.has('immich_album_id') ? 'immich_album_id' : null);
if (albumIdSource) {
const providerExpr = names.has('provider')
? "CASE WHEN provider IS NULL OR provider = '' THEN 'immich' ELSE provider END"
: "'immich'";
const albumNameExpr = names.has('album_name') ? "COALESCE(album_name, '')" : "''";
const syncEnabledExpr = names.has('sync_enabled') ? 'COALESCE(sync_enabled, 1)' : '1';
const lastSyncedExpr = names.has('last_synced_at') ? 'last_synced_at' : 'NULL';
const createdAtExpr = names.has('created_at') ? 'COALESCE(created_at, CURRENT_TIMESTAMP)' : 'CURRENT_TIMESTAMP';
db.exec(`
CREATE TABLE trip_album_links_new (
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,
provider TEXT NOT NULL,
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, provider, album_id)
);
`);
db.exec(`
INSERT OR IGNORE INTO trip_album_links_new (trip_id, user_id, provider, album_id, album_name, sync_enabled, last_synced_at, created_at)
SELECT trip_id, user_id, ${providerExpr}, ${albumIdSource}, ${albumNameExpr}, ${syncEnabledExpr}, ${lastSyncedExpr}, ${createdAtExpr}
FROM trip_album_links
WHERE ${albumIdSource} IS NOT NULL AND TRIM(${albumIdSource}) != ''
`);
db.exec('DROP TABLE trip_album_links');
db.exec('ALTER TABLE trip_album_links_new RENAME TO trip_album_links');
db.exec('CREATE INDEX IF NOT EXISTS idx_trip_album_links_trip ON trip_album_links(trip_id)');
}
}
},
() => {
// Add Synology credential columns for existing databases
try { db.exec('ALTER TABLE users ADD COLUMN synology_url TEXT'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
try { db.exec('ALTER TABLE users ADD COLUMN synology_username TEXT'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
try { db.exec('ALTER TABLE users ADD COLUMN synology_password TEXT'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
try { db.exec('ALTER TABLE users ADD COLUMN synology_sid TEXT'); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
},
() => {
// Seed Synology Photos provider and fields in existing databases
try {
db.prepare(`
INSERT INTO photo_providers (id, name, description, icon, enabled, sort_order)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
description = excluded.description,
icon = excluded.icon,
enabled = excluded.enabled,
sort_order = excluded.sort_order
`).run(
'synologyphotos',
'Synology Photos',
'Synology Photos integration with separate account settings',
'Image',
0,
1,
);
} catch (err: any) {
if (!err.message?.includes('no such table')) throw err;
}
try {
const insertField = db.prepare(`
INSERT INTO photo_provider_fields
(provider_id, field_key, label, input_type, placeholder, required, secret, settings_key, payload_key, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(provider_id, field_key) DO UPDATE SET
label = excluded.label,
input_type = excluded.input_type,
placeholder = excluded.placeholder,
required = excluded.required,
secret = excluded.secret,
settings_key = excluded.settings_key,
payload_key = excluded.payload_key,
sort_order = excluded.sort_order
`);
insertField.run('synologyphotos', 'synology_url', 'Server URL', 'url', 'https://synology.example.com', 1, 0, 'synology_url', 'synology_url', 0);
insertField.run('synologyphotos', 'synology_username', 'Username', 'text', 'Username', 1, 0, 'synology_username', 'synology_username', 1);
insertField.run('synologyphotos', 'synology_password', 'Password', 'password', 'Password', 1, 1, null, 'synology_password', 2);
} catch (err: any) {
if (!err.message?.includes('no such table')) throw err;
}
},
() => {
// Remove the stored config column from photo_providers now that it is generated from provider id.
const columns = db.prepare("PRAGMA table_info('photo_providers')").all() as Array<{ name: string }>;
const names = new Set(columns.map(c => c.name));
if (!names.has('config')) return;
db.exec('ALTER TABLE photo_providers DROP COLUMN config');
},
() => {
const columns = db.prepare("PRAGMA table_info('trip_photos')").all() as Array<{ name: string }>;
const names = new Set(columns.map(c => c.name));
if (names.has('asset_id') && !names.has('immich_asset_id')) return;
db.exec('ALTER TABLE `trip_photos` RENAME COLUMN immich_asset_id TO asset_id');
db.exec('ALTER TABLE `trip_photos` ADD COLUMN provider TEXT NOT NULL DEFAULT "immich"');
db.exec('ALTER TABLE `trip_album_links` ADD COLUMN provider TEXT NOT NULL DEFAULT "immich"');
db.exec('ALTER TABLE `trip_album_links` RENAME COLUMN immich_album_id TO album_id');
},
() => {
// Track which album link each photo was synced from
try { db.exec("ALTER TABLE trip_photos ADD COLUMN album_link_id INTEGER REFERENCES trip_album_links(id) ON DELETE SET NULL DEFAULT NULL"); } catch (err: any) { if (!err.message?.includes('duplicate column name')) throw err; }
+31
View File
@@ -18,6 +18,12 @@ function createTables(db: Database.Database): void {
mfa_enabled INTEGER DEFAULT 0,
mfa_secret TEXT,
mfa_backup_codes TEXT,
immich_url TEXT,
immich_access_token TEXT,
synology_url TEXT,
synology_username TEXT,
synology_password TEXT,
synology_sid TEXT,
must_change_password INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
@@ -162,6 +168,7 @@ function createTables(db: Database.Database): void {
place_id INTEGER REFERENCES places(id) ON DELETE SET NULL,
assignment_id INTEGER REFERENCES day_assignments(id) ON DELETE SET NULL,
title TEXT NOT NULL,
accommodation_id TEXT,
reservation_time TEXT,
reservation_end_time TEXT,
location TEXT,
@@ -222,6 +229,30 @@ function createTables(db: Database.Database): void {
sort_order INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS photo_providers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
icon TEXT DEFAULT 'Image',
enabled INTEGER DEFAULT 0,
sort_order INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS photo_provider_fields (
id INTEGER PRIMARY KEY AUTOINCREMENT,
provider_id TEXT NOT NULL REFERENCES photo_providers(id) ON DELETE CASCADE,
field_key TEXT NOT NULL,
label TEXT NOT NULL,
input_type TEXT NOT NULL DEFAULT 'text',
placeholder TEXT,
required INTEGER DEFAULT 0,
secret INTEGER DEFAULT 0,
settings_key TEXT,
payload_key TEXT,
sort_order INTEGER DEFAULT 0,
UNIQUE(provider_id, field_key)
);
-- Vacay addon tables
CREATE TABLE IF NOT EXISTS vacay_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
+33
View File
@@ -92,6 +92,39 @@ function seedAddons(db: Database.Database): void {
];
const insertAddon = db.prepare('INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)');
for (const a of defaultAddons) insertAddon.run(a.id, a.name, a.description, a.type, a.icon, a.enabled, a.sort_order);
const providerRows = [
{
id: 'immich',
name: 'Immich',
description: 'Immich photo provider',
icon: 'Image',
enabled: 0,
sort_order: 0,
},
{
id: 'synologyphotos',
name: 'Synology Photos',
description: 'Synology Photos integration with separate account settings',
icon: 'Image',
enabled: 0,
sort_order: 1,
},
];
const insertProvider = db.prepare('INSERT OR IGNORE INTO photo_providers (id, name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?)');
for (const p of providerRows) insertProvider.run(p.id, p.name, p.description, p.icon, p.enabled, p.sort_order);
const providerFields = [
{ provider_id: 'immich', field_key: 'immich_url', label: 'Immich URL', input_type: 'url', placeholder: 'https://immich.example.com', required: 1, secret: 0, settings_key: 'immich_url', payload_key: 'immich_url', sort_order: 0 },
{ provider_id: 'immich', field_key: 'immich_api_key', label: 'API Key', input_type: 'password', placeholder: 'API Key', required: 1, secret: 1, settings_key: null, payload_key: 'immich_api_key', sort_order: 1 },
{ provider_id: 'synologyphotos', field_key: 'synology_url', label: 'Server URL', input_type: 'url', placeholder: 'https://synology.example.com', required: 1, secret: 0, settings_key: 'synology_url', payload_key: 'synology_url', sort_order: 0 },
{ provider_id: 'synologyphotos', field_key: 'synology_username', label: 'Username', input_type: 'text', placeholder: 'Username', required: 1, secret: 0, settings_key: 'synology_username', payload_key: 'synology_username', sort_order: 1 },
{ provider_id: 'synologyphotos', field_key: 'synology_password', label: 'Password', input_type: 'password', placeholder: 'Password', required: 1, secret: 1, settings_key: null, payload_key: 'synology_password', sort_order: 2 },
];
const insertProviderField = db.prepare('INSERT OR IGNORE INTO photo_provider_fields (provider_id, field_key, label, input_type, placeholder, required, secret, settings_key, payload_key, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
for (const f of providerFields) {
insertProviderField.run(f.provider_id, f.field_key, f.label, f.input_type, f.placeholder, f.required, f.secret, f.settings_key, f.payload_key, f.sort_order);
}
console.log('Default addons seeded');
} catch (err: unknown) {
console.error('Error seeding addons:', err instanceof Error ? err.message : err);