Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of [CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md) for the full list. Highlights: client proofing links (#683), custom content pages (#738), asset picker search (#740), concurrent-edit detection (#742), env-locked settings (#739), an accessibility baseline (#741), three security fixes with advisories (#737, #755, #759), and a pre-release quality pass (#780–#797). --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Ralf <rohlenbo79@gmail.com> Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com> Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com> Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
106 lines
3.2 KiB
TypeScript
106 lines
3.2 KiB
TypeScript
/**
|
|
* Tell the photographer that a client submitted a proofing selection.
|
|
*
|
|
* One POST to `PROOFING_WEBHOOK_URL` with a JSON body shaped to be useful to
|
|
* as many receivers as possible without a per-service setting: Discord reads
|
|
* `content`, Slack and Mattermost read `text`, Gotify reads `title` and
|
|
* `message`, and n8n, Home Assistant or a script of your own get the
|
|
* structured `proofing` object.
|
|
*
|
|
* Fire-and-forget by design. A webhook that is down must never turn a client's
|
|
* submit into an error — the selection is already saved and visible in the
|
|
* admin panel — so failures are logged and swallowed, and the request is
|
|
* bounded by a short timeout.
|
|
*/
|
|
|
|
import type { ProofSession } from './proofSessions';
|
|
|
|
const WEBHOOK_TIMEOUT_MS = 5000;
|
|
|
|
export interface ProofWebhookPayload {
|
|
event: 'proofing.submitted';
|
|
title: string;
|
|
message: string;
|
|
text: string;
|
|
content: string;
|
|
proofing: {
|
|
id: string;
|
|
clientName: string;
|
|
albumName: string;
|
|
selected: number;
|
|
submittedAt: string;
|
|
/** The admin page for this session, when a site URL is configured. */
|
|
adminUrl: string | null;
|
|
};
|
|
}
|
|
|
|
export function buildWebhookPayload(
|
|
session: ProofSession,
|
|
albumName: string,
|
|
siteUrl: string | null,
|
|
): ProofWebhookPayload {
|
|
const count = session.selection.length;
|
|
const title = `Proofing: ${session.clientName} submitted a selection`;
|
|
const adminUrl = siteUrl ? `${siteUrl}/admin/proofing?session=${session.id}` : null;
|
|
const message =
|
|
`${session.clientName} picked ${count} ${count === 1 ? 'photo' : 'photos'} from "${albumName}".` +
|
|
(adminUrl ? ` ${adminUrl}` : '');
|
|
return {
|
|
event: 'proofing.submitted',
|
|
title,
|
|
message,
|
|
text: `${title}\n${message}`,
|
|
content: `**${title}**\n${message}`,
|
|
proofing: {
|
|
id: session.id,
|
|
clientName: session.clientName,
|
|
albumName,
|
|
selected: count,
|
|
submittedAt: session.submittedAt ?? new Date().toISOString(),
|
|
adminUrl,
|
|
},
|
|
};
|
|
}
|
|
|
|
/** Parse PROOFING_WEBHOOK_URL; anything that is not an http(s) URL is ignored. */
|
|
export function webhookUrl(raw = process.env.PROOFING_WEBHOOK_URL): string | null {
|
|
const value = raw?.trim();
|
|
if (!value) return null;
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function notifySubmitted(
|
|
session: ProofSession,
|
|
albumName: string,
|
|
siteUrl: string | null,
|
|
): Promise<void> {
|
|
const url = webhookUrl();
|
|
if (!url) return;
|
|
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), WEBHOOK_TIMEOUT_MS);
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(buildWebhookPayload(session, albumName, siteUrl)),
|
|
signal: controller.signal,
|
|
});
|
|
if (!res.ok) {
|
|
console.warn(`[Proofing] Webhook answered ${res.status}; the selection is saved regardless.`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(
|
|
'[Proofing] Webhook failed; the selection is saved regardless:',
|
|
err instanceof Error ? err.message : err,
|
|
);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|