contacts: Contrack as the robust contacts (C3), picked in Settings or the setup wizard
ci / check (push) Successful in 1m0s
ci / images (push) Successful in 32s

Spec 11 §4: contacts.provider and contacts.contrack (local address only, company token
write-only), each person's own token (users.contrack_token_enc, migration 0010) in My settings,
books read from Contrack with a 5-minute cache, Add to contacts and accepted shares created in
Contrack, the company account on desk phones. Setup wizard step 8 Contacts. deploy/contrack
(2.0.2, pinned), Caddy/nginx blocks, decision 0003, admin guide Contacts. Tested against a fake
and a real Contrack 2.0.2 (contrack-live.test.ts).
This commit is contained in:
iamdoubz committed 2026-10-23 15:23:49 -05:00
1 parent a754974a90
commit 692dd59552
32 files changed
+5787 -120

No files matched your search

+12 -1
View File
@@ -40,9 +40,20 @@ export type ContactData = {
};
export type ContactDto = ContactData & {
/** A uuid, or a Contrack contact id such as jane-smith-f60e8536. */
id: string;
/** mine: I edit it; company: read-only for users (admins edit it in Admin → Contacts). */
book: 'mine' | 'company';
/** Contrack: the contact's page there (editing happens in Contrack). */
url: string | null;
};
/** GET /api/me/contacts and /api/contacts: the book(s), and where they live. */
export type ContactBookDto = {
items: ContactDto[];
provider: 'builtin' | 'contrack';
/** Contrack mode: the address people open, and whether this book's token works. */
contrack: { publicUrl: string | null; connected: boolean; error: string | null } | null;
};
/** POST …/contacts/import: the files' text, read in the browser. */
@@ -66,7 +77,7 @@ export type ContactImportResult = {
export const ContactShareSchema = z
.object({
contactIds: z.array(z.uuid()).min(1).max(500),
contactIds: z.array(z.string().min(1).max(100)).min(1).max(500),
/** Colleagues by extension number. */
extensions: z.array(z.string().max(20)).max(500).default([]),
everyone: z.boolean().default(false),
+2
View File
@@ -55,4 +55,6 @@ export type MeDto = {
callReportTime: string;
callReportWeekday: number;
callReportSummaries: boolean;
/** Contacts in Contrack mode (11 §4.3): my token, never shown again; null in builtin mode. */
contrack: { connected: boolean; tokenPrefix: string | null; publicUrl: string | null } | null;
};
+29 -2
View File
@@ -104,6 +104,18 @@ export const SettingsSchemas = {
'ai.callTranscripts': z.boolean().default(false),
/** …and summarized by ai.llm. */
'ai.callSummaries': z.boolean().default(false),
/** Contacts (spec 11): the builtin books, or Contrack. */
'contacts.provider': z.enum(['builtin', 'contrack']).default('builtin'),
/** Contrack (11 §4.2): `url` the api calls (local only), `publicUrl` the one people open. */
'contacts.contrack': z
.object({
url: z.string().min(1),
publicUrl: z.string().nullable(),
/** The company account's personal token (11 §4.3), encrypted; '' = none. */
companyTokenEnc: z.string(),
})
.nullable()
.default(null),
// Not in 02 §3's table but used by the generator (03 §3).
'system.transportHash': z.string().nullable().default(null),
};
@@ -122,11 +134,26 @@ export const SmtpInputSchema = z.object({
});
export type SmtpInput = z.input<typeof SmtpInputSchema>;
/** GET /api/settings: every key, secrets replaced by hasPassword. */
/** PATCH /api/settings `contacts.contrack`: the company token is write-only (empty keeps it). */
export const ContrackInputSchema = z.object({
url: z.url({ protocol: /^https?$/ }).trim(),
publicUrl: z
.url({ protocol: /^https?$/ })
.trim()
.nullish()
.or(z.literal(''))
.transform((s) => s?.replace(/\/+$/, '') || null),
companyToken: z.string().trim().max(200).optional(),
clearCompanyToken: z.boolean().optional(),
});
export type ContrackInput = z.input<typeof ContrackInputSchema>;
/** GET /api/settings: every key, secrets replaced by hasPassword / hasCompanyToken. */
export type SettingsDto = {
[K in Exclude<SettingKey, 'smtp' | 'sms.webhookToken'>]: SettingValue<K>;
[K in Exclude<SettingKey, 'smtp' | 'sms.webhookToken' | 'contacts.contrack'>]: SettingValue<K>;
} & {
smtp: (Omit<SmtpInput, 'password'> & { hasPassword: boolean }) | null;
'contacts.contrack': { url: string; publicUrl: string | null; hasCompanyToken: boolean } | null;
};
/** POST /api/ai/test: one endpoint tried with a tiny request. `audio`: the TTS test, a data: URL. */