feat(immich): ask nothing of a server at the door

Adding a server no longer opens with a tick list: a fresh node keeps every
album its key can see — the empty album list the backend stores for "all of
them" — and narrowing is a change made on the node that is already there.
The album-ID field carries the warnings that were silent before it: a line
with no id in it, and an id this key never named, both said where they are
typed. A node's key can now be replaced in the dialog that narrows its
albums; the backend probes it first, so a key that does not answer leaves
the node and the dialog as they were.
This commit is contained in:
2026-10-10 16:38:28 +07:00
parent e27998d820
commit 8901e0c378
4 changed files with 164 additions and 34 deletions
+67 -17
View File
@@ -142,21 +142,29 @@ await context.route('**/api/**', (route) => {
return send(200, { ok: true, servers: saved.map(view) });
}
const b = body();
if (!b.url || !b.key) return send(400, { ok: false, error: 'invalid' });
const box = HOSTS[b.url];
// An edit names the node and says only what changes — a new key, or nothing
// at all — so what it does not repeat is what the node already has.
const at = b.id ? saved.findIndex((s) => s.id === b.id) : -1;
if (b.id && at < 0) return send(404, { ok: false, error: 'invalid' });
const had = at < 0 ? null : saved[at];
const url0 = b.url ?? had?.url;
const key0 = b.key ?? (had ? HOSTS[had.url]?.key : undefined);
if (!url0 || !key0) return send(400, { ok: false, error: 'invalid' });
const box = HOSTS[url0];
if (!box) return send(200, { ok: false, error: 'unreachable' });
if (box.key !== b.key) return send(200, { ok: false, error: 'auth' });
if (box.key !== key0) return send(200, { ok: false, error: 'auth' });
const server = {
id: `s${++minted}`,
name: String(b.name ?? '').trim() || new URL(b.url).hostname,
url: b.url,
type: b.type === 'share' ? 'share' : 'api',
id: had?.id ?? `s${++minted}`,
name: String(b.name ?? had?.name ?? '').trim() || new URL(url0).hostname,
url: url0,
type: (b.type ?? had?.type) === 'share' ? 'share' : 'api',
version: box.version,
canDownload: box.canDownload,
albums: cleanAlbums(b.albums),
keyMasked: `••••${String(b.key).slice(-4)}`,
albums: had && b.albums === undefined ? had.albums : cleanAlbums(b.albums),
keyMasked: `••••${String(key0).slice(-4)}`,
};
saved.push(server);
if (had) saved[at] = server;
else saved.push(server);
return send(200, { ok: true, server: view(server) });
}
@@ -299,15 +307,31 @@ await page.waitForSelector('[data-key="immich-status"]');
const saidOk = await page.$eval('[data-key="immich-status"]', (e) => !e.className.includes('err'));
step('a key that answers is said to be good', saidOk);
step(
'and what it offers is that server’s albums and no other server’s',
(await page.$('[data-key="immich-album-' + A1 + '"]')) !== null &&
(await page.$('[data-key="immich-album-' + A2 + '"]')) !== null &&
(await page.$('[data-key="immich-album-' + B1 + '"]')) === null
'and the door asks for nothing else: a fresh server is every album its key can see',
(await page.$('[data-key="immich-albums"]')) === null &&
(await page.$('[data-key="immich-album-' + A1 + '"]')) === null
);
// The ID field is the only place an album can be named at the door, and it says
// what it made of the lines: one with no id in it, and one this key never named.
await page.fill('[data-key="immich-ids"]', `khong-phai-uuid\n${B1}`);
await page.waitForSelector('[data-key="immich-bad-ids"]', { timeout: 5_000 }).catch(() => {});
step(
'a line with no album id in it is a line the field says it dropped',
(await page.$('[data-key="immich-bad-ids"]')) !== null &&
(await page.$eval('[data-key="immich-bad-ids"]', (e) => e.textContent)).includes('1'),
await page.$eval('[data-key="immich-bad-ids"]', (e) => e.textContent).catch(() => 'no warning')
);
step(
'with every one of them ticked to begin with',
(await page.isChecked('[data-key="immich-album-' + A1 + '"]')) &&
(await page.isChecked('[data-key="immich-album-' + A2 + '"]'))
'and an id this key cannot see is an id the field says it does not know',
(await page.$('[data-key="immich-unknown-ids"]')) !== null,
await page.$eval('[data-key="immich-unknown-ids"]', (e) => e.textContent).catch(() => 'no warning')
);
await page.fill('[data-key="immich-ids"]', '');
await page.waitForTimeout(200);
step(
'and clearing the field takes both warnings with it',
(await page.$('[data-key="immich-bad-ids"]')) === null &&
(await page.$('[data-key="immich-unknown-ids"]')) === null
);
await page.click('[data-key="immich-save"]');
await settle('Immich', 5);
@@ -365,6 +389,32 @@ step(
JSON.stringify(await counts())
);
// --- a key given to a node that is already there -----------------------------
// The one place a node's key is replaced: the same dialog that narrows its
// albums. It is probed before it is stored, so a key that does not answer is a
// dialog still open on a node unchanged.
await editDialog('Immich');
await page.fill('[data-key="immich-newkey"]', 'not-the-key');
await page.click('[data-key="immich-save"]');
await page.waitForSelector('[data-key="immich-error"]', { timeout: 5_000 }).catch(() => {});
step(
'a key given in place of the one it has is refused out loud, dialog still open',
(await page.$('[data-key="immich-error"]')) !== null &&
(await page.$('[data-key="immich-modal"]')) !== null,
await page.$eval('[data-key="immich-error"]', (e) => e.textContent).catch(() => 'no word said')
);
await page.fill('[data-key="immich-newkey"]', 'good-key');
await page.click('[data-key="immich-save"]');
await page.waitForSelector('[data-key="immich-modal"]', { state: 'detached', timeout: 10_000 }).catch(() => {});
await settle('Immich', 5);
await open('Immich');
step(
'and one that answers is taken, the node and its albums exactly as they were',
(await page.$('[data-key="immich-modal"]')) === null &&
same(await counts(), { Immich: '5', 'Immich/A1': '2', 'Immich/A2': '3' }),
JSON.stringify(await counts())
);
// --- a share link ------------------------------------------------------------
await addDialog('https://share.example.com', 'share-xyz', true);
step('a share link is not offered an album picker: it is one album', (await page.$('[data-key="immich-albums"]')) === null);