The session built: Appointments page: a single list, newest first. Block Time: edit and delete. Customer CSV export: with barber notes excluded. Guest-link regression test: a test that an appointment id alone is rejected, plus fixes to the stale docs. It built the scheduler, job runner with live progress, backup and restore, and the health alerts. Its own report says a real delete-and-restore on throwaway Docker passed, but I haven’t repeated that. It also says real SMTP and large dumps were not exercised. The manager image needs rebuilding for the backups to work, because it now installs 7zip.
This commit is contained in:
95 files changed
+7955
-150
No files matched your search
@@ -64,6 +64,44 @@ export async function sendTestSmtpEmail(config: TestEmailConfig): Promise<TestEm
|
||||
}
|
||||
}
|
||||
|
||||
export interface PlainEmailConfig extends Omit<TestEmailConfig, "recipient"> {
|
||||
recipient: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same transport + error-categorising contract as sendTestSmtpEmail, for
|
||||
* the operator alert emails (health / backup failures). Plain text only;
|
||||
* never throws and never echoes raw SMTP errors.
|
||||
*/
|
||||
export async function sendPlainSmtpEmail(config: PlainEmailConfig): Promise<TestEmailResult> {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
secure: config.encryption === "tls",
|
||||
ignoreTLS: config.encryption === "none",
|
||||
auth: { user: config.username, pass: config.password },
|
||||
connectionTimeout: 10_000,
|
||||
greetingTimeout: 10_000,
|
||||
socketTimeout: 20_000,
|
||||
});
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: config.fromName ? `"${config.fromName}" <${config.fromAddress}>` : config.fromAddress,
|
||||
to: config.recipient,
|
||||
replyTo: config.replyTo || undefined,
|
||||
subject: config.subject,
|
||||
text: config.text,
|
||||
});
|
||||
return { ok: true, message: `Sent to ${config.recipient}.` };
|
||||
} catch (error) {
|
||||
return { ok: false, message: categorizeSmtpError(error) };
|
||||
} finally {
|
||||
transporter.close();
|
||||
}
|
||||
}
|
||||
|
||||
function categorizeSmtpError(error: unknown): string {
|
||||
const code = typeof error === "object" && error !== null && "code" in error ? String((error as { code?: unknown }).code) : "";
|
||||
const responseCode =
|
||||
|
||||
Reference in new issue
Block a user