slotlock 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +542 -0
- package/NOTICE +2 -0
- package/README.md +4 -2
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +1068 -68
- package/application-api.d.ts +28 -1
- package/application-api.js +789 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +34 -8
- package/assistant.js +81 -26
- package/availability-store.d.ts +193 -0
- package/availability-store.js +566 -0
- package/booking-rules.d.ts +103 -5
- package/booking-rules.js +263 -15
- package/config.d.ts +50 -0
- package/config.js +107 -0
- package/dashboard-assistant-ui.d.ts +1 -1
- package/dashboard-assistant-ui.js +109 -8
- package/dashboard-scope.d.ts +7 -0
- package/dashboard-scope.js +21 -0
- package/dashboard.d.ts +152 -8
- package/dashboard.js +3868 -399
- package/ddl-identity.d.ts +25 -0
- package/ddl-identity.js +881 -0
- package/ddl-scim.d.ts +5 -0
- package/ddl-scim.js +740 -0
- package/ddl-sso.d.ts +15 -0
- package/ddl-sso.js +1033 -0
- package/ddl.d.ts +44 -9
- package/ddl.js +2872 -47
- package/domains.d.ts +117 -0
- package/domains.js +340 -0
- package/event-feed.d.ts +2 -2
- package/event-feed.js +24 -34
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +56 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +209 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +24 -5
- package/index.js +17 -3
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/invitation-email.d.ts +26 -0
- package/invitation-email.js +73 -0
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +26 -2
- package/maintenance.js +70 -2
- package/metrics.js +3 -0
- package/oidc.d.ts +135 -0
- package/oidc.js +452 -0
- package/openapi.json +20172 -9850
- package/package.json +2 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/scim-server.d.ts +19 -0
- package/scim-server.js +243 -0
- package/scim.d.ts +180 -0
- package/scim.js +416 -0
- package/self-host.d.ts +19 -6
- package/self-host.js +483 -28
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +637 -23
- package/store.js +2529 -147
- package/types.d.ts +230 -0
- package/types.js +68 -1
- package/workspace-domains.d.ts +146 -0
- package/workspace-domains.js +249 -0
- package/workspace-roles.d.ts +47 -0
- package/workspace-roles.js +304 -0
- package/workspace-scim.d.ts +89 -0
- package/workspace-scim.js +157 -0
- package/workspace-sso.d.ts +131 -0
- package/workspace-sso.js +180 -0
- package/workspaces.d.ts +240 -0
- package/workspaces.js +433 -0
package/config.js
CHANGED
|
@@ -181,6 +181,14 @@ export function readDashboardConfig(env) {
|
|
|
181
181
|
if (Array.isArray(orgs) && googleClientId !== undefined) {
|
|
182
182
|
throw new SlotlockConfigError('SLOTLOCK_GOOGLE_CLIENT_ID does not go with a list of SLOTLOCK_DASHBOARD_ORGS: those organizations are GitHub\'s; use SLOTLOCK_DASHBOARD_ORGS=enabled to have both');
|
|
183
183
|
}
|
|
184
|
+
const teamWorkspaces = readDashboardTeamWorkspaces(env);
|
|
185
|
+
if (teamWorkspaces && Array.isArray(orgs)) {
|
|
186
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_TEAM_WORKSPACES does not go with a list of SLOTLOCK_DASHBOARD_ORGS: with a list, everyone works in those organizations');
|
|
187
|
+
}
|
|
188
|
+
const invitationEmail = readInvitationEmail(env, teamWorkspaces);
|
|
189
|
+
const verifiedDomains = readVerifiedDomains(env);
|
|
190
|
+
const teamSso = readTeamSso(env, verifiedDomains !== undefined);
|
|
191
|
+
const teamScim = readTeamScim(env, verifiedDomains !== undefined);
|
|
184
192
|
const name = optional(env, 'SLOTLOCK_DASHBOARD_NAME')?.trim();
|
|
185
193
|
if (name !== undefined && name !== '' && !SLOTLOCK_DASHBOARD_NAME_PATTERN.test(name)) {
|
|
186
194
|
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_NAME must be 1-60 characters, without <, > or control or invisible characters');
|
|
@@ -196,6 +204,11 @@ export function readDashboardConfig(env) {
|
|
|
196
204
|
? { google: { clientId: googleClientId, clientSecret: googleClientSecret } }
|
|
197
205
|
: {}),
|
|
198
206
|
...(orgs !== undefined ? { organizations: orgs } : {}),
|
|
207
|
+
...(teamWorkspaces ? { teamWorkspaces: true } : {}),
|
|
208
|
+
...(invitationEmail ? { invitationEmail } : {}),
|
|
209
|
+
...(verifiedDomains ? { verifiedDomains } : {}),
|
|
210
|
+
...(teamSso ? { teamSso } : {}),
|
|
211
|
+
...(teamScim ? { teamScim } : {}),
|
|
199
212
|
...(name !== undefined && name !== '' ? { name } : {}),
|
|
200
213
|
...(termsUrl !== undefined ? { termsUrl } : {}),
|
|
201
214
|
...(privacyUrl !== undefined ? { privacyUrl } : {}),
|
|
@@ -265,6 +278,100 @@ export function readDashboardOrganizations(env) {
|
|
|
265
278
|
}
|
|
266
279
|
return [...new Set(orgs.split(','))];
|
|
267
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* SLOTLOCK_INVITATION_EMAIL (resend or postmark), with its key and the verified sender: all three, or
|
|
283
|
+
* none. Only with team workspaces, the one thing it sends.
|
|
284
|
+
*/
|
|
285
|
+
function readInvitationEmail(env, teamWorkspaces) {
|
|
286
|
+
const provider = optional(env, 'SLOTLOCK_INVITATION_EMAIL');
|
|
287
|
+
const key = optional(env, 'SLOTLOCK_INVITATION_EMAIL_KEY');
|
|
288
|
+
const from = optional(env, 'SLOTLOCK_INVITATION_EMAIL_FROM');
|
|
289
|
+
if (provider === undefined && key === undefined && from === undefined)
|
|
290
|
+
return undefined;
|
|
291
|
+
if (provider !== 'resend' && provider !== 'postmark') {
|
|
292
|
+
throw new SlotlockConfigError('SLOTLOCK_INVITATION_EMAIL must be resend or postmark');
|
|
293
|
+
}
|
|
294
|
+
if (!teamWorkspaces) {
|
|
295
|
+
throw new SlotlockConfigError('SLOTLOCK_INVITATION_EMAIL sends team invitations: it needs SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on');
|
|
296
|
+
}
|
|
297
|
+
if (key === undefined || !/^[\x21-\x7e]{10,500}$/.test(key)) {
|
|
298
|
+
throw new SlotlockConfigError(`SLOTLOCK_INVITATION_EMAIL_KEY must be ${provider === 'resend' ? "Resend's API key" : "Postmark's server token"}`);
|
|
299
|
+
}
|
|
300
|
+
if (from === undefined || !/^([^<>\r\n"]{1,80} )?<?[^@\s<>"]{1,64}@[^@\s<>"]+\.[^@\s<>"]+>?$/.test(from)) {
|
|
301
|
+
throw new SlotlockConfigError('SLOTLOCK_INVITATION_EMAIL_FROM must be the sender the provider verified: team@example.com or Name <team@example.com>');
|
|
302
|
+
}
|
|
303
|
+
return { provider, key, from };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* SLOTLOCK_VERIFIED_DOMAINS: on or off (the default), only with team workspaces; and
|
|
307
|
+
* SLOTLOCK_DNS_RESOLVER, how a domain's record is looked up: doh (the default: Cloudflare's
|
|
308
|
+
* DNS-over-HTTPS, cross-checked by Google's) or system (the server's own resolver, which follows
|
|
309
|
+
* split-horizon DNS). Read on its own as well: the server that runs maintenance checks the domains
|
|
310
|
+
* each day, wherever the dashboard runs.
|
|
311
|
+
*/
|
|
312
|
+
export function readVerifiedDomains(env) {
|
|
313
|
+
const value = optional(env, 'SLOTLOCK_VERIFIED_DOMAINS') ?? 'off';
|
|
314
|
+
if (value !== 'on' && value !== 'off') {
|
|
315
|
+
throw new SlotlockConfigError('SLOTLOCK_VERIFIED_DOMAINS must be on or off');
|
|
316
|
+
}
|
|
317
|
+
const resolver = optional(env, 'SLOTLOCK_DNS_RESOLVER');
|
|
318
|
+
if (resolver !== undefined && resolver !== 'doh' && resolver !== 'system') {
|
|
319
|
+
throw new SlotlockConfigError('SLOTLOCK_DNS_RESOLVER must be doh or system');
|
|
320
|
+
}
|
|
321
|
+
if (value === 'off') {
|
|
322
|
+
if (resolver !== undefined) {
|
|
323
|
+
throw new SlotlockConfigError('SLOTLOCK_DNS_RESOLVER looks up verified domains: it needs SLOTLOCK_VERIFIED_DOMAINS=on');
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
if (!readDashboardTeamWorkspaces(env)) {
|
|
328
|
+
throw new SlotlockConfigError('SLOTLOCK_VERIFIED_DOMAINS verifies team workspaces\' domains: it needs SLOTLOCK_DASHBOARD_TEAM_WORKSPACES=on');
|
|
329
|
+
}
|
|
330
|
+
return { resolver: resolver ?? 'doh' };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* SLOTLOCK_TEAM_SSO: on or off (the default), only with verified domains; with it,
|
|
334
|
+
* SLOTLOCK_SSO_SECRET (32+ characters), which seals each team's client secret.
|
|
335
|
+
*/
|
|
336
|
+
function readTeamSso(env, verifiedDomains) {
|
|
337
|
+
const value = optional(env, 'SLOTLOCK_TEAM_SSO') ?? 'off';
|
|
338
|
+
if (value !== 'on' && value !== 'off')
|
|
339
|
+
throw new SlotlockConfigError('SLOTLOCK_TEAM_SSO must be on or off');
|
|
340
|
+
const secret = optional(env, 'SLOTLOCK_SSO_SECRET');
|
|
341
|
+
if (value === 'off') {
|
|
342
|
+
if (secret !== undefined)
|
|
343
|
+
throw new SlotlockConfigError('SLOTLOCK_SSO_SECRET seals teams\' client secrets: it needs SLOTLOCK_TEAM_SSO=on');
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
if (!verifiedDomains) {
|
|
347
|
+
throw new SlotlockConfigError('SLOTLOCK_TEAM_SSO trusts addresses only at verified domains: it needs SLOTLOCK_VERIFIED_DOMAINS=on');
|
|
348
|
+
}
|
|
349
|
+
if (secret === undefined || secret.length < 32 || /\s/.test(secret)) {
|
|
350
|
+
throw new SlotlockConfigError(`SLOTLOCK_SSO_SECRET must be 32 or more characters without spaces. ${SECRET_HINT}`);
|
|
351
|
+
}
|
|
352
|
+
return { secret };
|
|
353
|
+
}
|
|
354
|
+
/** SLOTLOCK_TEAM_SCIM: on or off (the default), only with verified domains. */
|
|
355
|
+
function readTeamScim(env, verifiedDomains) {
|
|
356
|
+
const value = optional(env, 'SLOTLOCK_TEAM_SCIM') ?? 'off';
|
|
357
|
+
if (value !== 'on' && value !== 'off')
|
|
358
|
+
throw new SlotlockConfigError('SLOTLOCK_TEAM_SCIM must be on or off');
|
|
359
|
+
if (value === 'on' && !verifiedDomains) {
|
|
360
|
+
throw new SlotlockConfigError('SLOTLOCK_TEAM_SCIM adds people only at verified domains: it needs SLOTLOCK_VERIFIED_DOMAINS=on');
|
|
361
|
+
}
|
|
362
|
+
return value === 'on';
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* SLOTLOCK_DASHBOARD_TEAM_WORKSPACES: on or off (the default). Read on its own as well: a server whose
|
|
366
|
+
* dashboard runs elsewhere (on Cloudflare Workers) takes team workspaces' keys only with it on.
|
|
367
|
+
*/
|
|
368
|
+
export function readDashboardTeamWorkspaces(env) {
|
|
369
|
+
const value = optional(env, 'SLOTLOCK_DASHBOARD_TEAM_WORKSPACES') ?? 'off';
|
|
370
|
+
if (value !== 'on' && value !== 'off') {
|
|
371
|
+
throw new SlotlockConfigError('SLOTLOCK_DASHBOARD_TEAM_WORKSPACES must be on or off');
|
|
372
|
+
}
|
|
373
|
+
return value === 'on';
|
|
374
|
+
}
|
|
268
375
|
/** The token buckets `slotlock serve` keeps (SLOTLOCK_LIMITS), each counted per its own key. */
|
|
269
376
|
export const SLOTLOCK_RATE_NAMES = Object.freeze([
|
|
270
377
|
'address',
|
|
@@ -10,7 +10,7 @@ export declare function slotlockAssistantActionVerb(operation: string): {
|
|
|
10
10
|
danger: boolean;
|
|
11
11
|
};
|
|
12
12
|
/** The chat's styles: one centred column, the composer docked under it, quiet steps and cards. */
|
|
13
|
-
export declare const SLOTLOCK_ASSISTANT_CSS = "\n.assistant-page { display: grid; }\n.assistant { position: relative; display: flex; flex-direction: column; width: min(100%, 720px); margin: 0 auto; min-height: calc(100dvh - 180px); }\n.assistant .thread { flex: 1; }\n.assistant[data-empty] { justify-content: center; padding-bottom: 12vh; }\n.assistant[data-empty] .thread { display: none; }\n.assistant[data-empty] .dock { order: 1; position: static; padding: 0; background: none; }\n.assistant[data-empty] .assistant-start { order: 2; padding-top: var(--space-3); }\n.assistant:not([data-empty]) .assistant-start { display: none; }\n.chips { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); }\n.chips form { margin: 0; }\n.chip { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-full); border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); font: inherit; font-size: var(--text-md); cursor: pointer; transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease); }\n.chip:hover { background: var(--hover); border-color: var(--line-strong); color: var(--ink); }\n.thread { list-style: none; margin: 0; padding: var(--space-2) 0 var(--space-6); display: grid; gap: var(--space-6); align-content: start; }\n.turn { margin: 0; min-width: 0; }\n.turn.mine { justify-self: end; max-width: 80%; }\n.bubble { padding: 10px 14px; border-radius: 18px; background: var(--sunken); color: var(--ink); font-size: var(--text-lg); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }\n.turn.bot { justify-self: stretch; }\n.prose { max-width: 68ch; color: var(--ink); font-size: var(--text-lg); line-height: 1.6; overflow-wrap: anywhere; }\n.prose > :first-child { margin-top: 0; }\n.prose > :last-child { margin-bottom: 0; }\n.prose p { margin: 0 0 var(--space-3); color: inherit; }\n.prose strong { font-weight: 600; }\n.prose h4 { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-lg); font-weight: 600; }\n.prose ul, .prose ol { margin: 0 0 var(--space-3); padding-left: 20px; }\n.prose li { margin: 4px 0; }\n.prose li > ul, .prose li > ol { margin: 4px 0 0; }\n.prose a { color: var(--accent); text-underline-offset: 2px; }\n.prose code { font: 400 12.5px/1.4 var(--mono); background: var(--sunken); border-radius: 4px; padding: 1px 5px; }\n.prose pre { margin: 0 0 var(--space-3); padding: var(--space-3); background: var(--sunken); border: 1px solid var(--line); border-radius: var(--radius); overflow-x: auto; }\n.prose pre code { background: none; padding: 0; font-size: 12.5px; }\n.prose blockquote { margin: 0 0 var(--space-3); padding-left: var(--space-3); border-left: 2px solid var(--line-strong); color: var(--ink-2); }\n.prose hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-4) 0; }\n.md-table { overflow-x: auto; margin: 0 0 var(--space-3); }\n.md-table table { border-collapse: collapse; font-size: var(--text-md); min-width: 50%; }\n.md-table table { width: auto; }\n.md-table th { font: 600 var(--text-md)/1.4 var(--font); text-transform: none; letter-spacing: 0; background: none; text-align: left; color: var(--ink-2); padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }\n.md-table td { padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }\n.md-table tr:last-child td { border-bottom: 0; }\n.change details.more { margin: 0; }\n.change details.more > summary { cursor: pointer; color: var(--ink-3); font-size: var(--text-sm); list-style: none; }\n.change details.more > summary::-webkit-details-marker { display: none; }\n.change details.more > summary:hover { color: var(--ink-2); }\n.change details.more[open] > summary { margin-bottom: var(--space-2); }\n.turn.steps { justify-self: start; }\ndetails.steps > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--ink-3); font-size: var(--text-md); padding: 2px 0; }\ndetails.steps > summary::-webkit-details-marker { display: none; }\ndetails.steps > summary::after { content: \"\"; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform var(--fast) var(--ease); margin-left: 2px; }\ndetails.steps[open] > summary::after { transform: rotate(45deg); }\ndetails.steps > summary:hover { color: var(--ink-2); }\ndetails.steps.has-bad > summary::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: var(--bad); }\n.step-list { list-style: none; margin: var(--space-2) 0 0; padding: 0 0 0 var(--space-3); border-left: 1px solid var(--line); display: grid; gap: 6px; }\n.step { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }\n.step::before { content: \"\"; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--good); transform: translateY(-1px); }\n.step.bad::before { background: var(--bad); }\n.step.run::before { background: var(--ink-3); }\n.step .why { color: var(--ink-3); font-size: var(--text-sm); }\n.live-steps { display: grid; gap: 6px; }\n.live-steps .step.run, .thinking { background: linear-gradient(90deg, var(--ink-3) 0%, var(--ink) 50%, var(--ink-3) 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: assistant-shimmer 1.6s linear infinite; }\n.thinking { font-size: var(--text-md); justify-self: start; }\n@keyframes assistant-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n.prose.streaming > :last-child::after { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-left: 4px; border-radius: 50%; background: var(--ink-3); vertical-align: baseline; animation: assistant-pulse 1s ease-in-out infinite; }\n@keyframes assistant-pulse { 50% { opacity: 0.25; } }\n.turn.note { color: var(--ink-3); font-size: var(--text-md); }\n.turn.error { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 10px 12px; border: 1px solid var(--bad-line); background: var(--bad-wash); border-radius: var(--radius); font-size: var(--text-md); color: var(--ink); }\n.turn.error::before { content: \"\"; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--bad); }\n.turn.error > span { flex: 1; }\n.turn.error form, .turn.note form { margin: 0; }\n.text-button { appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: var(--text-md); color: var(--accent); cursor: pointer; }\n.text-button:hover { text-decoration: underline; text-underline-offset: 2px; }\n.turn.action { justify-self: start; width: min(100%, 560px); }\n.change { padding: var(--space-4) var(--space-5); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); display: grid; gap: var(--space-3); }\n.change-title { margin: 0; font-size: var(--text-lg); font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }\n.change .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px var(--space-4); margin: 0; font-size: var(--text-md); }\n.change .facts dt { color: var(--ink-3); font-size: var(--text-sm); padding-top: 1px; }\n.change .facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }\n.change .facts dd pre { margin: 0; white-space: pre-wrap; font: 400 12px/1.5 var(--mono); }\n.change .facts .was { color: var(--ink-3); text-decoration: line-through; }\n.change-buttons { display: flex; gap: var(--space-2); flex-wrap: wrap; }\n.change-buttons form { margin: 0; }\n.change-buttons button { height: 32px; }\nbutton.danger { background: var(--bad); color: var(--bad-ink); border-color: var(--bad); }\nbutton.danger:hover { filter: brightness(1.08); }\nbutton.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }\nbutton.ghost:hover { background: var(--hover); color: var(--ink); }\n.receipt { margin: 0; display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }\n.receipt::before { content: \"\"; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); transform: translateY(-1px); }\n.receipt.ok { color: var(--ink); }\n.receipt.ok::before { background: var(--good); }\n.receipt.bad::before { background: var(--bad); }\n.receipt .why { color: var(--ink-3); }\n.dock { position: sticky; bottom: 0; padding: var(--space-6) 0 var(--space-6); background: linear-gradient(to bottom, transparent, var(--canvas) 24px); z-index: 2; }\n.composer { display: grid; margin: 0; background: var(--surface); border: 1px solid var(--line); border-radius: 16px; transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease); }\n.composer:focus-within { border-color: var(--line-strong); box-shadow: 0 0 0 3px var(--ring); }\n.composer textarea { display: block; width: 100%; min-height: 26px; max-height: 200px; resize: none; border: 0; outline: 0; background: transparent; padding: 14px 16px 0; color: var(--ink); font: inherit; font-size: var(--text-lg); line-height: 1.5; box-shadow: none; }\n.composer textarea:focus { box-shadow: none; border: 0; }\n.composer-bar { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); padding: var(--space-2); }\n.composer-bar .count { margin-right: auto; padding-left: var(--space-2); font-size: var(--text-sm); color: var(--ink-3); }\n.composer-bar .count.over { color: var(--warn); }\n.send { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 0; background: var(--button); color: var(--button-ink); cursor: pointer; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }\n.send:hover { background: var(--button-hover); }\n.send:disabled { opacity: 0.4; cursor: default; }\n.send svg { width: 16px; height: 16px; }\n.send .stop { display: none; }\n.assistant.streaming .send .arrow { display: none; }\n.assistant.streaming .send .stop { display: block; }\n.jump { position: absolute; left: 50%; transform: translate(-50%, 0); top: -8px; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); display: grid; place-items: center; cursor: pointer; box-shadow: var(--pop-shadow); }\n.jump[hidden] { display: none; }\n.new-chat svg { width: 14px; height: 14px; }\n.assistant-page[data-empty] .new-chat { display: none; }\n@media (prefers-reduced-motion: reduce) {\n .live-steps .step.run, .thinking { animation: none; color: var(--ink-3); background: none; }\n .prose.streaming > :last-child::after { animation: none; }\n}\n@media (max-width: 760px) {\n .assistant { min-height: calc(100dvh - 140px); }\n .assistant[data-empty] { justify-content: flex-end; padding-bottom: 0; }\n .assistant[data-empty] .assistant-start { order: 1; padding: 0 0 var(--space-3); }\n .assistant[data-empty] .dock { order: 2; position: sticky; padding-bottom: max(12px, env(safe-area-inset-bottom)); }\n .chips { flex-wrap: nowrap; justify-content: flex-start; overflow-x: auto; max-width: 100%; padding-bottom: 2px; scrollbar-width: none; }\n .chips::-webkit-scrollbar { display: none; }\n .chip { flex: none; }\n .turn.mine { max-width: 88%; }\n .md-table table { display: table; }\n .md-table thead { display: table-header-group; }\n .md-table tbody { display: table-row-group; }\n .md-table tr { display: table-row; padding: 0; border: 0; }\n .md-table th, .md-table td { display: table-cell; padding: 6px 12px 6px 0; }\n .md-table td { border-bottom: 1px solid var(--line-soft); }\n .composer textarea { font-size: 16px; }\n .dock { padding-bottom: max(12px, env(safe-area-inset-bottom)); }\n .change { padding: var(--space-4); }\n .change .facts { grid-template-columns: minmax(0, 1fr); gap: 2px; }\n .change .facts dd { margin-bottom: var(--space-2); }\n .change-buttons form { flex: 1; }\n .change-buttons button { width: 100%; height: 40px; }\n}\n";
|
|
13
|
+
export declare const SLOTLOCK_ASSISTANT_CSS = "\n.assistant-page { display: grid; }\n.assistant { position: relative; display: flex; flex-direction: column; width: min(100%, 720px); margin: 0 auto; min-height: calc(100dvh - 180px); }\n.assistant .thread { flex: 1; }\n.assistant[data-empty] { justify-content: center; padding-bottom: 12vh; }\n.assistant[data-empty] .thread { display: none; }\n.assistant[data-empty] .dock { order: 1; position: static; padding: 0; background: none; }\n.assistant[data-empty] .assistant-start { order: 2; padding-top: var(--space-3); }\n.assistant:not([data-empty]) .assistant-start { display: none; }\n.chips { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); }\n.chips form { margin: 0; }\n.chip { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-full); border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); font: inherit; font-size: var(--text-md); cursor: pointer; transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease); }\n.chip:hover { background: var(--hover); border-color: var(--line-strong); color: var(--ink); }\n.thread { list-style: none; margin: 0; padding: var(--space-2) 0 var(--space-6); display: grid; gap: var(--space-6); align-content: start; }\n.turn { margin: 0; min-width: 0; }\n.turn.mine { justify-self: end; max-width: 80%; }\n.bubble { padding: 10px 14px; border-radius: 18px; background: var(--sunken); color: var(--ink); font-size: var(--text-lg); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }\n.turn.bot { justify-self: stretch; }\n.prose { max-width: 68ch; color: var(--ink); font-size: var(--text-lg); line-height: 1.6; overflow-wrap: anywhere; }\n.prose > :first-child { margin-top: 0; }\n.prose > :last-child { margin-bottom: 0; }\n.prose p { margin: 0 0 var(--space-3); color: inherit; }\n.prose strong { font-weight: 600; }\n.prose h4 { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-lg); font-weight: 600; }\n.prose ul, .prose ol { margin: 0 0 var(--space-3); padding-left: 20px; }\n.prose li { margin: 4px 0; }\n.prose li > ul, .prose li > ol { margin: 4px 0 0; }\n.prose a { color: var(--accent); text-underline-offset: 2px; }\n.prose code { font: 400 12.5px/1.4 var(--mono); background: var(--sunken); border-radius: 4px; padding: 1px 5px; }\n.prose pre { margin: 0 0 var(--space-3); padding: var(--space-3); background: var(--sunken); border: 1px solid var(--line); border-radius: var(--radius); overflow-x: auto; }\n.prose pre code { background: none; padding: 0; font-size: 12.5px; }\n.prose blockquote { margin: 0 0 var(--space-3); padding-left: var(--space-3); border-left: 2px solid var(--line-strong); color: var(--ink-2); }\n.prose hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-4) 0; }\n.md-table { overflow-x: auto; margin: 0 0 var(--space-3); }\n.md-table table { border-collapse: collapse; font-size: var(--text-md); min-width: 50%; }\n.md-table table { width: auto; }\n.md-table th { font: 600 var(--text-md)/1.4 var(--font); text-transform: none; letter-spacing: 0; background: none; text-align: left; color: var(--ink-2); padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }\n.md-table td { padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }\n.md-table tr:last-child td { border-bottom: 0; }\n.change details.more { margin: 0; }\n.change details.more > summary { cursor: pointer; color: var(--ink-3); font-size: var(--text-sm); list-style: none; }\n.change details.more > summary::-webkit-details-marker { display: none; }\n.change details.more > summary:hover { color: var(--ink-2); }\n.change details.more[open] > summary { margin-bottom: var(--space-2); }\n.turn.steps { justify-self: start; }\ndetails.steps > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--ink-3); font-size: var(--text-md); padding: 2px 0; }\ndetails.steps > summary::-webkit-details-marker { display: none; }\ndetails.steps > summary::after { content: \"\"; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform var(--fast) var(--ease); margin-left: 2px; }\ndetails.steps[open] > summary::after { transform: rotate(45deg); }\ndetails.steps > summary:hover { color: var(--ink-2); }\ndetails.steps.has-bad > summary::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: var(--bad); }\n.step-list { list-style: none; margin: var(--space-2) 0 0; padding: 0 0 0 var(--space-3); border-left: 1px solid var(--line); display: grid; gap: 6px; }\n.step { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }\n.step::before { content: \"\"; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--good); transform: translateY(-1px); }\n.step.bad::before { background: var(--bad); }\n.step.run::before { background: var(--ink-3); }\n.step .why { color: var(--ink-3); font-size: var(--text-sm); }\n.live-steps { display: grid; gap: 6px; }\n.live-steps .step.run, .thinking { background: linear-gradient(90deg, var(--ink-3) 0%, var(--ink) 50%, var(--ink-3) 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: assistant-shimmer 1.6s linear infinite; }\n.thinking { font-size: var(--text-md); justify-self: start; }\n@keyframes assistant-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n.prose.streaming > :last-child::after { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-left: 4px; border-radius: 50%; background: var(--ink-3); vertical-align: baseline; animation: assistant-pulse 1s ease-in-out infinite; }\n@keyframes assistant-pulse { 50% { opacity: 0.25; } }\n.turn.note { color: var(--ink-3); font-size: var(--text-md); }\n.turn.error { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 10px 12px; border: 1px solid var(--bad-line); background: var(--bad-wash); border-radius: var(--radius); font-size: var(--text-md); color: var(--ink); }\n.turn.error::before { content: \"\"; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--bad); }\n.turn.error > span { flex: 1; }\n.turn.error form, .turn.note form { margin: 0; }\n.text-button { appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: var(--text-md); color: var(--accent); cursor: pointer; }\n.text-button:hover { text-decoration: underline; text-underline-offset: 2px; }\n.turn.action { justify-self: start; width: min(100%, 560px); }\n.change { padding: var(--space-4) var(--space-5); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); display: grid; gap: var(--space-3); }\n.change-title { margin: 0; font-size: var(--text-lg); font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }\n.change .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px var(--space-4); margin: 0; font-size: var(--text-md); }\n.change .facts dt { color: var(--ink-3); font-size: var(--text-sm); padding-top: 1px; }\n.change .facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }\n.change .facts dd pre { margin: 0; white-space: pre-wrap; font: 400 12px/1.5 var(--mono); }\n.change .facts .was { color: var(--ink-3); text-decoration: line-through; }\n.change-buttons { display: flex; gap: var(--space-2); flex-wrap: wrap; }\n.change-buttons form { margin: 0; }\n.change-buttons button { height: 32px; }\nbutton.danger { background: var(--bad); color: var(--bad-ink); border-color: var(--bad); }\nbutton.danger:hover { filter: brightness(1.08); }\nbutton.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }\nbutton.ghost:hover { background: var(--hover); color: var(--ink); }\n.receipt { margin: 0; display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }\n.receipt::before { content: \"\"; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); transform: translateY(-1px); }\n.receipt.ok { color: var(--ink); }\n.receipt.ok::before { background: var(--good); }\n.receipt.bad::before { background: var(--bad); }\n.receipt .why { color: var(--ink-3); }\n.dock { position: sticky; bottom: 0; padding: var(--space-6) 0 var(--space-6); background: linear-gradient(to bottom, transparent, var(--canvas) 24px); z-index: 2; }\n.composer { display: grid; margin: 0; background: var(--surface); border: 1px solid var(--line); border-radius: 16px; transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease); }\n.composer:focus-within { border-color: var(--line-strong); box-shadow: 0 0 0 3px var(--ring); }\n.composer textarea { display: block; width: 100%; min-height: 26px; max-height: 200px; resize: none; border: 0; outline: 0; background: transparent; padding: 14px 16px 0; color: var(--ink); font: inherit; font-size: var(--text-lg); line-height: 1.5; box-shadow: none; }\n.composer textarea:focus { box-shadow: none; border: 0; }\n.composer-bar { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); padding: var(--space-2); }\n.composer-bar .count { margin-right: auto; padding-left: var(--space-2); font-size: var(--text-sm); color: var(--ink-3); }\n.composer-bar .count.over { color: var(--warn); }\n.send { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 0; background: var(--button); color: var(--button-ink); cursor: pointer; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }\n.send:hover { background: var(--button-hover); }\n.send:disabled { opacity: 0.4; cursor: default; }\n.send svg { width: 16px; height: 16px; }\n.send .stop { display: none; }\n.assistant.streaming .send .arrow { display: none; }\n.assistant.streaming .send .stop { display: block; }\n.jump { position: absolute; left: 50%; transform: translate(-50%, 0); top: -8px; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); display: grid; place-items: center; cursor: pointer; box-shadow: var(--pop-shadow); }\n.jump[hidden] { display: none; }\n.new-chat svg, .chats-link svg { width: 14px; height: 14px; }\n.assistant-page[data-empty] .new-chat { display: none; }\n.assistant-actions { display: flex; gap: var(--space-2); }\n.chats-link { display: none; }\n/* Chats: the member's conversations beside the one shown, most recent first, grouped by day. */\n.assistant-shell { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: var(--space-6); align-items: start; }\n/* It scrolls with the page: a column that scrolled on its own would clip a row's menu. */\n.chats { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }\n.chats-head { display: flex; align-items: center; justify-content: space-between; min-height: 36px; }\n.chats-head h2 { margin: 0; font-size: var(--text-md); font-weight: 600; color: var(--ink-2); }\n.icon-button { display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm); color: var(--ink-2); transition: background var(--fast) var(--ease), color var(--fast) var(--ease); }\n.icon-button:hover { background: var(--hover); color: var(--ink); text-decoration: none; }\n.icon-button svg { width: 15px; height: 15px; }\n.chats-filter { height: 32px; font-size: var(--text-md); }\n.chats-filter[hidden] { display: none; }\n/* Sentence case, not the dashboard's uppercase h3. */\n.chat-group { margin: var(--space-3) 0 2px; padding: 0 var(--space-2); font-family: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1.5; text-transform: none; letter-spacing: 0; color: var(--ink-3); }\n.chat-group[hidden] { display: none; }\n.chat-list { min-width: 0; }\n/* minmax(0, 1fr): a long title is cut with an ellipsis, never widening the column. */\n.chat-rows { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; }\n.chat-row { position: relative; display: flex; align-items: center; border-radius: var(--radius); transition: background var(--fast) var(--ease); }\n.chat-row[hidden] { display: none; }\n.chat-row:hover, .chat-row:focus-within, .chat-row.current { background: var(--hover); }\n.chat-link { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--space-2); padding: 7px 4px 7px var(--space-2); color: var(--ink-2); font-size: var(--text-md); }\n.chat-link:hover { color: var(--ink); text-decoration: none; }\n.chat-row.current .chat-link { color: var(--ink); font-weight: 500; }\n.chat-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.chat-when { flex: none; font-size: var(--text-xs); color: var(--ink-3); font-variant-numeric: tabular-nums; }\n.chat-menu { position: relative; flex: none; }\n.chat-menu summary { list-style: none; display: grid; place-items: center; width: 28px; height: 28px; margin-right: 2px; border-radius: var(--radius-sm); color: var(--ink-3); cursor: pointer; opacity: 0; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }\n.chat-menu summary::-webkit-details-marker { display: none; }\n.chat-row:hover .chat-menu summary, .chat-row:focus-within .chat-menu summary, .chat-row.current .chat-menu summary, .chat-menu[open] summary { opacity: 1; }\n.chat-menu summary:hover, .chat-menu[open] summary { background: var(--line-soft); color: var(--ink); }\n.chat-menu summary svg { width: 14px; height: 14px; }\n.chat-menu .pop { z-index: 20; width: 260px; display: grid; gap: var(--space-2); }\n.chat-menu .pop form { display: grid; gap: var(--space-2); margin: 0; }\n.chat-menu .pop form + form { padding-top: var(--space-2); border-top: 1px solid var(--line-soft); }\n.chat-menu .pop button.danger { background: none; border-color: transparent; box-shadow: none; color: var(--bad); }\n.chat-menu .pop button.danger:hover { background: var(--bad-wash); }\n.chat-rename input { width: 100%; min-width: 0; height: 34px; }\n.chats-empty { padding: var(--space-2); font-size: var(--text-md); }\n.chats-page .chats { max-width: 560px; }\n.chats-page .chats-head { display: none; }\n.chats-page .chat-menu summary { opacity: 1; }\n@media (max-width: 1100px) {\n .assistant-shell { grid-template-columns: minmax(0, 1fr); }\n .assistant-shell > .chats { display: none; }\n .chats-link { display: inline-flex; }\n}\n@media (hover: none) {\n .chat-menu summary { opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) {\n .live-steps .step.run, .thinking { animation: none; color: var(--ink-3); background: none; }\n .prose.streaming > :last-child::after { animation: none; }\n}\n@media (max-width: 760px) {\n .assistant { min-height: calc(100dvh - 140px); }\n .assistant[data-empty] { justify-content: flex-end; padding-bottom: 0; }\n .assistant[data-empty] .assistant-start { order: 1; padding: 0 0 var(--space-3); }\n .assistant[data-empty] .dock { order: 2; position: sticky; padding-bottom: max(12px, env(safe-area-inset-bottom)); }\n .chips { flex-wrap: nowrap; justify-content: flex-start; overflow-x: auto; max-width: 100%; padding-bottom: 2px; scrollbar-width: none; }\n .chips::-webkit-scrollbar { display: none; }\n .chip { flex: none; }\n .turn.mine { max-width: 88%; }\n .md-table table { display: table; }\n .md-table thead { display: table-header-group; }\n .md-table tbody { display: table-row-group; }\n .md-table tr { display: table-row; padding: 0; border: 0; }\n .md-table th, .md-table td { display: table-cell; padding: 6px 12px 6px 0; }\n .md-table td { border-bottom: 1px solid var(--line-soft); }\n .composer textarea { font-size: 16px; }\n .dock { padding-bottom: max(12px, env(safe-area-inset-bottom)); }\n .change { padding: var(--space-4); }\n .change .facts { grid-template-columns: minmax(0, 1fr); gap: 2px; }\n .change .facts dd { margin-bottom: var(--space-2); }\n .change-buttons form { flex: 1; }\n .change-buttons button { width: 100%; height: 40px; }\n}\n";
|
|
14
14
|
/**
|
|
15
15
|
* The browser side of the chat, appended to app.js: Enter sends, the box grows, a write chip fills
|
|
16
16
|
* the box instead of sending, and a reply streams in from `<root>/assistant/stream` (newline-
|
|
@@ -6,6 +6,7 @@ const STEP_WORDS = Object.freeze({
|
|
|
6
6
|
slotlock_get_free_busy: ['Checking free and busy time', 'Checked free and busy time', 'check free and busy time'],
|
|
7
7
|
slotlock_find_next_available: ['Finding the next free time', 'Found the next free time', 'find the next free time'],
|
|
8
8
|
slotlock_find_available_slots: ['Checking availability', 'Checked availability', 'check availability'],
|
|
9
|
+
slotlock_find_available_ranges: ['Finding free ranges', 'Found free ranges', 'find free ranges'],
|
|
9
10
|
slotlock_get_pool_availability: ['Checking the pool', 'Checked the pool', 'check the pool'],
|
|
10
11
|
slotlock_get_event: ['Reading a booking', 'Read a booking', 'read the booking'],
|
|
11
12
|
slotlock_list_events: ['Reading bookings', 'Read bookings', 'read bookings'],
|
|
@@ -43,6 +44,7 @@ export function slotlockAssistantActionVerb(operation) {
|
|
|
43
44
|
case 'slotlock_delete_event':
|
|
44
45
|
return { verb: 'Delete booking', busy: 'Deleting…', done: 'Deleted', danger: true };
|
|
45
46
|
case 'slotlock_hold_slots':
|
|
47
|
+
case 'slotlock_hold_together':
|
|
46
48
|
return { verb: 'Hold', busy: 'Holding…', done: 'Held', danger: false };
|
|
47
49
|
case 'slotlock_release_slots':
|
|
48
50
|
return { verb: 'Release', busy: 'Releasing…', done: 'Released', danger: true };
|
|
@@ -159,8 +161,59 @@ button.ghost:hover { background: var(--hover); color: var(--ink); }
|
|
|
159
161
|
.assistant.streaming .send .stop { display: block; }
|
|
160
162
|
.jump { position: absolute; left: 50%; transform: translate(-50%, 0); top: -8px; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); display: grid; place-items: center; cursor: pointer; box-shadow: var(--pop-shadow); }
|
|
161
163
|
.jump[hidden] { display: none; }
|
|
162
|
-
.new-chat svg { width: 14px; height: 14px; }
|
|
164
|
+
.new-chat svg, .chats-link svg { width: 14px; height: 14px; }
|
|
163
165
|
.assistant-page[data-empty] .new-chat { display: none; }
|
|
166
|
+
.assistant-actions { display: flex; gap: var(--space-2); }
|
|
167
|
+
.chats-link { display: none; }
|
|
168
|
+
/* Chats: the member's conversations beside the one shown, most recent first, grouped by day. */
|
|
169
|
+
.assistant-shell { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: var(--space-6); align-items: start; }
|
|
170
|
+
/* It scrolls with the page: a column that scrolled on its own would clip a row's menu. */
|
|
171
|
+
.chats { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
|
|
172
|
+
.chats-head { display: flex; align-items: center; justify-content: space-between; min-height: 36px; }
|
|
173
|
+
.chats-head h2 { margin: 0; font-size: var(--text-md); font-weight: 600; color: var(--ink-2); }
|
|
174
|
+
.icon-button { display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm); color: var(--ink-2); transition: background var(--fast) var(--ease), color var(--fast) var(--ease); }
|
|
175
|
+
.icon-button:hover { background: var(--hover); color: var(--ink); text-decoration: none; }
|
|
176
|
+
.icon-button svg { width: 15px; height: 15px; }
|
|
177
|
+
.chats-filter { height: 32px; font-size: var(--text-md); }
|
|
178
|
+
.chats-filter[hidden] { display: none; }
|
|
179
|
+
/* Sentence case, not the dashboard's uppercase h3. */
|
|
180
|
+
.chat-group { margin: var(--space-3) 0 2px; padding: 0 var(--space-2); font-family: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1.5; text-transform: none; letter-spacing: 0; color: var(--ink-3); }
|
|
181
|
+
.chat-group[hidden] { display: none; }
|
|
182
|
+
.chat-list { min-width: 0; }
|
|
183
|
+
/* minmax(0, 1fr): a long title is cut with an ellipsis, never widening the column. */
|
|
184
|
+
.chat-rows { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; }
|
|
185
|
+
.chat-row { position: relative; display: flex; align-items: center; border-radius: var(--radius); transition: background var(--fast) var(--ease); }
|
|
186
|
+
.chat-row[hidden] { display: none; }
|
|
187
|
+
.chat-row:hover, .chat-row:focus-within, .chat-row.current { background: var(--hover); }
|
|
188
|
+
.chat-link { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--space-2); padding: 7px 4px 7px var(--space-2); color: var(--ink-2); font-size: var(--text-md); }
|
|
189
|
+
.chat-link:hover { color: var(--ink); text-decoration: none; }
|
|
190
|
+
.chat-row.current .chat-link { color: var(--ink); font-weight: 500; }
|
|
191
|
+
.chat-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
192
|
+
.chat-when { flex: none; font-size: var(--text-xs); color: var(--ink-3); font-variant-numeric: tabular-nums; }
|
|
193
|
+
.chat-menu { position: relative; flex: none; }
|
|
194
|
+
.chat-menu summary { list-style: none; display: grid; place-items: center; width: 28px; height: 28px; margin-right: 2px; border-radius: var(--radius-sm); color: var(--ink-3); cursor: pointer; opacity: 0; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }
|
|
195
|
+
.chat-menu summary::-webkit-details-marker { display: none; }
|
|
196
|
+
.chat-row:hover .chat-menu summary, .chat-row:focus-within .chat-menu summary, .chat-row.current .chat-menu summary, .chat-menu[open] summary { opacity: 1; }
|
|
197
|
+
.chat-menu summary:hover, .chat-menu[open] summary { background: var(--line-soft); color: var(--ink); }
|
|
198
|
+
.chat-menu summary svg { width: 14px; height: 14px; }
|
|
199
|
+
.chat-menu .pop { z-index: 20; width: 260px; display: grid; gap: var(--space-2); }
|
|
200
|
+
.chat-menu .pop form { display: grid; gap: var(--space-2); margin: 0; }
|
|
201
|
+
.chat-menu .pop form + form { padding-top: var(--space-2); border-top: 1px solid var(--line-soft); }
|
|
202
|
+
.chat-menu .pop button.danger { background: none; border-color: transparent; box-shadow: none; color: var(--bad); }
|
|
203
|
+
.chat-menu .pop button.danger:hover { background: var(--bad-wash); }
|
|
204
|
+
.chat-rename input { width: 100%; min-width: 0; height: 34px; }
|
|
205
|
+
.chats-empty { padding: var(--space-2); font-size: var(--text-md); }
|
|
206
|
+
.chats-page .chats { max-width: 560px; }
|
|
207
|
+
.chats-page .chats-head { display: none; }
|
|
208
|
+
.chats-page .chat-menu summary { opacity: 1; }
|
|
209
|
+
@media (max-width: 1100px) {
|
|
210
|
+
.assistant-shell { grid-template-columns: minmax(0, 1fr); }
|
|
211
|
+
.assistant-shell > .chats { display: none; }
|
|
212
|
+
.chats-link { display: inline-flex; }
|
|
213
|
+
}
|
|
214
|
+
@media (hover: none) {
|
|
215
|
+
.chat-menu summary { opacity: 1; }
|
|
216
|
+
}
|
|
164
217
|
@media (prefers-reduced-motion: reduce) {
|
|
165
218
|
.live-steps .step.run, .thinking { animation: none; color: var(--ink-3); background: none; }
|
|
166
219
|
.prose.streaming > :last-child::after { animation: none; }
|
|
@@ -198,6 +251,29 @@ button.ghost:hover { background: var(--hover); color: var(--ink); }
|
|
|
198
251
|
*/
|
|
199
252
|
export function slotlockAssistantScript(markdownSource) {
|
|
200
253
|
return `
|
|
254
|
+
// Search chats: rows whose title holds what is typed (ignoring case and accents) stay shown.
|
|
255
|
+
function filterChats() {
|
|
256
|
+
var filter = document.getElementById('chats-filter');
|
|
257
|
+
if (!filter) return;
|
|
258
|
+
filter.hidden = false;
|
|
259
|
+
var fold = function (text) { return text.normalize('NFKD').replace(/\\p{M}/gu, '').toLowerCase(); };
|
|
260
|
+
var apply = function () {
|
|
261
|
+
var wanted = fold(filter.value.trim());
|
|
262
|
+
for (var group of document.querySelectorAll('.chat-rows')) {
|
|
263
|
+
var shown = 0;
|
|
264
|
+
for (var row of group.children) {
|
|
265
|
+
var title = row.querySelector('.chat-title');
|
|
266
|
+
row.hidden = wanted !== '' && fold(title ? title.textContent : '').indexOf(wanted) === -1;
|
|
267
|
+
if (!row.hidden) shown += 1;
|
|
268
|
+
}
|
|
269
|
+
var label = group.previousElementSibling;
|
|
270
|
+
if (label && label.classList.contains('chat-group')) label.hidden = shown === 0;
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
filter.addEventListener('input', apply);
|
|
274
|
+
apply();
|
|
275
|
+
}
|
|
276
|
+
filterChats();
|
|
201
277
|
(function () {
|
|
202
278
|
var page = document.querySelector('[data-assistant]');
|
|
203
279
|
if (!page || typeof fetch !== 'function' || typeof TextDecoder !== 'function') return;
|
|
@@ -267,14 +343,20 @@ export function slotlockAssistantScript(markdownSource) {
|
|
|
267
343
|
event.preventDefault();
|
|
268
344
|
if (!busy && box.value.trim() !== '') form.requestSubmit();
|
|
269
345
|
});
|
|
270
|
-
|
|
346
|
+
// A chip that asks sends through the box, so its reply streams; one that writes only fills it.
|
|
347
|
+
for (var chip of page.querySelectorAll('.chips button.chip')) {
|
|
271
348
|
chip.addEventListener('click', function (event) {
|
|
272
349
|
event.preventDefault();
|
|
273
|
-
|
|
350
|
+
var fill = this.getAttribute('data-fill');
|
|
351
|
+
box.value = fill !== null ? fill : (this.value || '');
|
|
274
352
|
grow();
|
|
275
353
|
update();
|
|
276
|
-
|
|
277
|
-
|
|
354
|
+
if (fill !== null) {
|
|
355
|
+
box.focus();
|
|
356
|
+
box.setSelectionRange(box.value.length, box.value.length);
|
|
357
|
+
} else if (!busy && box.value.trim() !== '') {
|
|
358
|
+
form.requestSubmit();
|
|
359
|
+
}
|
|
278
360
|
});
|
|
279
361
|
}
|
|
280
362
|
for (var decide of document.querySelectorAll('.change-buttons form')) {
|
|
@@ -323,7 +405,8 @@ export function slotlockAssistantScript(markdownSource) {
|
|
|
323
405
|
var prose = null;
|
|
324
406
|
var proseText = '';
|
|
325
407
|
var steps = null;
|
|
326
|
-
|
|
408
|
+
// Keyed by the provider's call id: no id, __proto__ included, may name an inherited property.
|
|
409
|
+
var stepNodes = Object.create(null);
|
|
327
410
|
var frame = 0;
|
|
328
411
|
follow(was);
|
|
329
412
|
function draw() {
|
|
@@ -374,6 +457,18 @@ export function slotlockAssistantScript(markdownSource) {
|
|
|
374
457
|
added = [];
|
|
375
458
|
thread.insertAdjacentHTML('beforeend', e.html || '');
|
|
376
459
|
setOnce(e.once);
|
|
460
|
+
// The conversation it went to: a new one has its id now, its own address, and a place in Chats.
|
|
461
|
+
if (e.chat) {
|
|
462
|
+
for (var field of page.querySelectorAll('input[name="chat"]')) field.value = e.chat;
|
|
463
|
+
if (e.url && window.history && history.replaceState) history.replaceState(null, '', e.url);
|
|
464
|
+
var heading = document.querySelector('.assistant-page .page-head h1');
|
|
465
|
+
if (heading && e.title) heading.textContent = e.title;
|
|
466
|
+
var panel = document.querySelector('aside.chats');
|
|
467
|
+
if (panel && e.chats) {
|
|
468
|
+
panel.outerHTML = e.chats;
|
|
469
|
+
filterChats();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
377
472
|
}
|
|
378
473
|
follow(was);
|
|
379
474
|
}
|
|
@@ -386,7 +481,8 @@ export function slotlockAssistantScript(markdownSource) {
|
|
|
386
481
|
}).then(function (response) {
|
|
387
482
|
var type = response.headers.get('content-type') || '';
|
|
388
483
|
if (!response.body || type.indexOf('application/x-ndjson') !== 0) {
|
|
389
|
-
|
|
484
|
+
var current = form.querySelector('input[name="chat"]');
|
|
485
|
+
location.assign(page.getAttribute('data-page') + (current && current.value ? '?c=' + current.value : '') + '#latest');
|
|
390
486
|
location.reload();
|
|
391
487
|
return;
|
|
392
488
|
}
|
|
@@ -432,7 +528,12 @@ export function slotlockAssistantScript(markdownSource) {
|
|
|
432
528
|
});
|
|
433
529
|
grow();
|
|
434
530
|
update();
|
|
435
|
-
|
|
531
|
+
// To the latest message only when it would be behind the box: a short conversation keeps its
|
|
532
|
+
// heading (and Chats) in view.
|
|
533
|
+
var dockAtLoad = page.querySelector('.dock');
|
|
534
|
+
var lastAtLoad = thread.lastElementChild;
|
|
535
|
+
var behind = !!lastAtLoad && lastAtLoad.getBoundingClientRect().bottom > window.innerHeight - (dockAtLoad ? dockAtLoad.offsetHeight : 0);
|
|
536
|
+
if (!page.hasAttribute('data-empty') && behind) window.scrollTo(0, document.documentElement.scrollHeight);
|
|
436
537
|
})();
|
|
437
538
|
`;
|
|
438
539
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SlotlockResourceScope } from './resource-profile.js';
|
|
2
|
+
/** Serve one dashboard request with a scope of its own, none until the person's is read. */
|
|
3
|
+
export declare function runSlotlockDashboardRequest<T>(serve: () => T): T;
|
|
4
|
+
/** The request's person is limited to `scope`: what is read for them from now on keeps to it. */
|
|
5
|
+
export declare function setSlotlockDashboardRequestScope(scope: SlotlockResourceScope | null): void;
|
|
6
|
+
/** The scope of the dashboard request being served, or null for none (or outside a request). */
|
|
7
|
+
export declare function slotlockDashboardRequestScope(): SlotlockResourceScope | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// The locations and groups the person signed in to the dashboard is limited to (RFC 0004 D1), for
|
|
2
|
+
// the request being served. The dashboard sets it once it has read the person's standing; the
|
|
3
|
+
// store its resources read through (createSlotlockDashboardResources) runs each transaction with it
|
|
4
|
+
// as its resource scope, so the database's resource_scope_v1 policies keep every read and write to
|
|
5
|
+
// those resources, as they do for a key limited to them.
|
|
6
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
|
+
const requests = new AsyncLocalStorage();
|
|
8
|
+
/** Serve one dashboard request with a scope of its own, none until the person's is read. */
|
|
9
|
+
export function runSlotlockDashboardRequest(serve) {
|
|
10
|
+
return requests.run({ scope: null }, serve);
|
|
11
|
+
}
|
|
12
|
+
/** The request's person is limited to `scope`: what is read for them from now on keeps to it. */
|
|
13
|
+
export function setSlotlockDashboardRequestScope(scope) {
|
|
14
|
+
const request = requests.getStore();
|
|
15
|
+
if (request)
|
|
16
|
+
request.scope = scope;
|
|
17
|
+
}
|
|
18
|
+
/** The scope of the dashboard request being served, or null for none (or outside a request). */
|
|
19
|
+
export function slotlockDashboardRequestScope() {
|
|
20
|
+
return requests.getStore()?.scope ?? null;
|
|
21
|
+
}
|
package/dashboard.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { SlotlockDashboardTeams } from './teams.js';
|
|
2
|
+
import type { SlotlockDashboardWorkspaces } from './workspaces.js';
|
|
3
|
+
import { type SlotlockDnsResolver } from './domains.js';
|
|
4
|
+
import { type SlotlockSecretBox } from './oidc.js';
|
|
5
|
+
import type { SlotlockAuditEvent, SlotlockAuditEventInput } from './store.js';
|
|
2
6
|
import { type SlotlockApiKeyStore } from './api-keys.js';
|
|
3
7
|
import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type SlotlockAgentOperationDispatchOptions, type SlotlockAgentPrincipal, type SlotlockAgentUsage } from './agent-server.js';
|
|
4
8
|
import { type SlotlockAssistantStore } from './assistant.js';
|
|
@@ -8,9 +12,12 @@ import { type SlotlockPlan, type SlotlockTenantPlan } from './plans.js';
|
|
|
8
12
|
import { type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceProfileInput } from './resource-profile.js';
|
|
9
13
|
import { type SlotlockUsageRow } from './usage.js';
|
|
10
14
|
import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
|
|
11
|
-
import type
|
|
15
|
+
import { type SlotlockSql, type SlotlockStore, type SlotlockSubject, type SlotlockSubjectErasure, type SlotlockSubjectExport, type SlotlockTenantExport, type SlotlockTenantExportPartResult, type SlotlockWaitlistEntry } from './store.js';
|
|
16
|
+
import type { SlotlockAvailabilityDefaults, SlotlockAvailabilityExceptionInput, SlotlockHolidayCalendar, SlotlockOverrideResources } from './availability-store.js';
|
|
17
|
+
import { type SlotlockAvailabilityException } from './hours.js';
|
|
18
|
+
import { type SlotlockHolidaySource } from './holiday-import.js';
|
|
12
19
|
import { type SlotlockSandboxScenario } from './sandbox.js';
|
|
13
|
-
import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
|
|
20
|
+
import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockContact, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
|
|
14
21
|
/** Resources one dashboard tenant may hold. */
|
|
15
22
|
export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
|
|
16
23
|
/**
|
|
@@ -18,6 +25,12 @@ export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
|
|
|
18
25
|
* person who made it (`github:<user id>`) as `actor`, to record on the event feed.
|
|
19
26
|
*/
|
|
20
27
|
export interface SlotlockDashboardResources {
|
|
28
|
+
/**
|
|
29
|
+
* These adapters keep each request to the locations and groups its member is limited to (RFC
|
|
30
|
+
* 0004 D1), as createSlotlockDashboardResources's do (dashboard-scope.ts). Without it, a limited
|
|
31
|
+
* member reaches only their own account.
|
|
32
|
+
*/
|
|
33
|
+
keepsToScope?: boolean;
|
|
21
34
|
list(tenantRef: string): Promise<SlotlockResource[]>;
|
|
22
35
|
/** How many resources the tenant has, archived ones included, as a resource limit counts them. */
|
|
23
36
|
count?(tenantRef: string): Promise<number>;
|
|
@@ -30,13 +43,64 @@ export interface SlotlockDashboardResources {
|
|
|
30
43
|
*/
|
|
31
44
|
add(tenantRef: string, externalRef: string, timezone: string, actor?: string): Promise<SlotlockResource>;
|
|
32
45
|
/**
|
|
33
|
-
* Set the bookable hours of one of the tenant's resources (`{ id }`)
|
|
34
|
-
*
|
|
35
|
-
*
|
|
46
|
+
* Set the bookable hours of one of the tenant's resources (`{ id }`), one of its locations
|
|
47
|
+
* (`{ location }`), or the tenant's own (`'all'`), which every resource without its own inherits
|
|
48
|
+
* (RFC 0004 B2); `null` hands them to what is above and `[]` closes. For the tenant's, `override`
|
|
49
|
+
* says what becomes of resources' own hours (SlotlockOverrideResources: `none` keeps them).
|
|
50
|
+
* Returns 0 for an id or location that is not the tenant's, else at least 1 (for the tenant's, how
|
|
51
|
+
* many resources they now apply to).
|
|
36
52
|
*/
|
|
37
53
|
setHours(tenantRef: string, target: {
|
|
38
54
|
id: string;
|
|
39
|
-
} |
|
|
55
|
+
} | {
|
|
56
|
+
location: string;
|
|
57
|
+
} | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string, override?: SlotlockOverrideResources): Promise<number>;
|
|
58
|
+
/**
|
|
59
|
+
* What the tenant keeps, exported whole, and what it keeps about one person, exported or erased
|
|
60
|
+
* (RFC 0004 D3): Settings, Privacy.
|
|
61
|
+
*/
|
|
62
|
+
privacy?: {
|
|
63
|
+
/** Ask for an export, built in the background; refused while one is built, or with five kept. */
|
|
64
|
+
createExport(tenantRef: string, actor?: string): Promise<SlotlockTenantExport>;
|
|
65
|
+
listExports(tenantRef: string): Promise<SlotlockTenantExport[]>;
|
|
66
|
+
exportPart(tenantRef: string, exportId: string, part: number): Promise<SlotlockTenantExportPartResult>;
|
|
67
|
+
deleteExport(tenantRef: string, exportId: string): Promise<boolean>;
|
|
68
|
+
exportSubject(tenantRef: string, subject: SlotlockSubject): Promise<SlotlockSubjectExport>;
|
|
69
|
+
eraseSubject(tenantRef: string, subject: SlotlockSubject, actor?: string): Promise<SlotlockSubjectErasure>;
|
|
70
|
+
};
|
|
71
|
+
/** The people the tenant keeps to tell about bookings (RFC 0004 P3), and forgetting one. */
|
|
72
|
+
contacts?: {
|
|
73
|
+
list(tenantRef: string, limit: number, after?: string): Promise<SlotlockContact[]>;
|
|
74
|
+
forget(tenantRef: string, ref: string, actor?: string): Promise<{
|
|
75
|
+
deleted: boolean;
|
|
76
|
+
bookings: number;
|
|
77
|
+
}>;
|
|
78
|
+
};
|
|
79
|
+
/** The tenant's stored hours and its closures and special hours (RFC 0004 B1, B2). */
|
|
80
|
+
hours?: {
|
|
81
|
+
defaults(tenantRef: string): Promise<SlotlockAvailabilityDefaults>;
|
|
82
|
+
exceptions(tenantRef: string, limit: number): Promise<SlotlockAvailabilityException[]>;
|
|
83
|
+
addException(tenantRef: string, exception: Omit<SlotlockAvailabilityExceptionInput, 'tenantRef'>, actor?: string): Promise<SlotlockAvailabilityException>;
|
|
84
|
+
deleteException(tenantRef: string, id: string, actor?: string): Promise<boolean>;
|
|
85
|
+
/**
|
|
86
|
+
* Import a holiday calendar (its text, or a URL with `byUrl`): each all-day event closes its
|
|
87
|
+
* days for the next two years. Kept by name at its scope, so importing it again updates it and
|
|
88
|
+
* removes the holidays it dropped; `subscribe` imports a URL again every 30 days.
|
|
89
|
+
*/
|
|
90
|
+
importHolidays(tenantRef: string, source: SlotlockHolidaySource, scope: {
|
|
91
|
+
location?: string;
|
|
92
|
+
resourceId?: string;
|
|
93
|
+
}, actor?: string): Promise<{
|
|
94
|
+
imported: number;
|
|
95
|
+
skipped: number;
|
|
96
|
+
removed: number;
|
|
97
|
+
}>;
|
|
98
|
+
/** Whether importHolidays reads calendars by URL. */
|
|
99
|
+
byUrl?: boolean;
|
|
100
|
+
/** The tenant's holiday calendars, and deleting one with its closures (false: not the tenant's). */
|
|
101
|
+
calendars?(tenantRef: string): Promise<SlotlockHolidayCalendar[]>;
|
|
102
|
+
deleteCalendar?(tenantRef: string, id: string, actor?: string): Promise<boolean>;
|
|
103
|
+
};
|
|
40
104
|
/** What occupies the tenant's resources in `window`, by start: the overview's agenda. */
|
|
41
105
|
agenda?(tenantRef: string, window: Interval, limit: number): Promise<SlotlockAgendaEntry[]>;
|
|
42
106
|
/** The tenant's latest changes, newest first: the overview's activity. */
|
|
@@ -133,6 +197,27 @@ export interface SlotlockDashboardResources {
|
|
|
133
197
|
};
|
|
134
198
|
/** The tenant's assistant settings and its members' conversations (assistant.ts). */
|
|
135
199
|
assistant?: SlotlockAssistantStore;
|
|
200
|
+
/**
|
|
201
|
+
* The tenant's audit log (slotlock.audit_events): who changed keys, webhooks, billing and
|
|
202
|
+
* settings, and every refusal. Append-only: record, and read newest first.
|
|
203
|
+
*/
|
|
204
|
+
audit?: {
|
|
205
|
+
/** Whether the workspace's chain holds from its oldest event kept (store verifyAuditChain). */
|
|
206
|
+
verify?(tenantRef: string): Promise<{
|
|
207
|
+
checked: number;
|
|
208
|
+
brokenAt: number | null;
|
|
209
|
+
}>;
|
|
210
|
+
record(tenantRef: string, event: SlotlockAuditEventInput): Promise<void>;
|
|
211
|
+
list(tenantRef: string, params?: {
|
|
212
|
+
limit?: number;
|
|
213
|
+
before?: {
|
|
214
|
+
occurredAt: Date;
|
|
215
|
+
id: string;
|
|
216
|
+
};
|
|
217
|
+
action?: string;
|
|
218
|
+
actorRef?: string;
|
|
219
|
+
}): Promise<SlotlockAuditEvent[]>;
|
|
220
|
+
};
|
|
136
221
|
/** The tenant's webhook endpoints, as the event feed keeps them. */
|
|
137
222
|
webhooks?: {
|
|
138
223
|
list(tenantRef: string): Promise<SlotlockWebhookEndpoint[]>;
|
|
@@ -229,7 +314,10 @@ export interface SlotlockDashboardAssistantOptions {
|
|
|
229
314
|
turnTimeoutMs?: number;
|
|
230
315
|
/** Model calls one message may make (8 by default, at most 20). */
|
|
231
316
|
maxSteps?: number;
|
|
232
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* How long a conversation is kept after its last message (1 second to 3,650 days). Without it, a
|
|
319
|
+
* member keeps each conversation until they delete it, up to SLOTLOCK_ASSISTANT_MAX_CHATS.
|
|
320
|
+
*/
|
|
233
321
|
conversationTtlMs?: number;
|
|
234
322
|
}
|
|
235
323
|
/**
|
|
@@ -306,6 +394,18 @@ export interface SlotlockDashboardBilling {
|
|
|
306
394
|
/** The https origins Upgrade and Manage redirect to, allowed in the policy's form-action. */
|
|
307
395
|
formOrigins: readonly string[];
|
|
308
396
|
}
|
|
397
|
+
/** Sends an invitation's link by email (hosted Slotlock); without one, the person inviting sends it. */
|
|
398
|
+
export interface SlotlockInvitationSender {
|
|
399
|
+
send(invitation: {
|
|
400
|
+
to: string;
|
|
401
|
+
/** The team workspace's name, and how the person inviting is shown. */
|
|
402
|
+
workspace: string;
|
|
403
|
+
inviter: string;
|
|
404
|
+
role: string;
|
|
405
|
+
link: string;
|
|
406
|
+
expiresAt: Date;
|
|
407
|
+
}): Promise<void>;
|
|
408
|
+
}
|
|
309
409
|
export interface SlotlockDashboardOptions {
|
|
310
410
|
/** Where people reach the server; the dashboard lives at `<publicUrl>/dashboard`. */
|
|
311
411
|
publicUrl: string;
|
|
@@ -373,6 +473,45 @@ export interface SlotlockDashboardOptions {
|
|
|
373
473
|
organizations?: readonly string[] | 'enabled';
|
|
374
474
|
/** The organizations turned on, with `organizations: 'enabled'` (teams.ts). */
|
|
375
475
|
teams?: SlotlockDashboardTeams;
|
|
476
|
+
/**
|
|
477
|
+
* Who belongs to which workspace, and as what (workspaces.ts, workspace-roles.ts): read on every
|
|
478
|
+
* request, so a role changed or a membership ended applies at the next one, and "Sign out
|
|
479
|
+
* everywhere" ends every session. Without it, a person is the Owner of their own workspace, and an
|
|
480
|
+
* organization's GitHub owners are its Owners and its other members Developers, as read at sign-in.
|
|
481
|
+
*/
|
|
482
|
+
workspaces?: SlotlockDashboardWorkspaces;
|
|
483
|
+
/**
|
|
484
|
+
* Team workspaces (`team:<id>`): anyone signed in creates one, invites people to it by email with
|
|
485
|
+
* a role, and changes or removes them (workspaces.ts teams). Needs `workspaces` with its
|
|
486
|
+
* database, and no fixed list of organizations. Off unless set.
|
|
487
|
+
*/
|
|
488
|
+
teamWorkspaces?: boolean;
|
|
489
|
+
/**
|
|
490
|
+
* Verified domains (workspace-domains.ts): a team's Owner proves a domain with a DNS record, and
|
|
491
|
+
* the people whose verified address is at it may ask to join. Needs team workspaces. The resolvers
|
|
492
|
+
* look the record up when an Owner asks (domains.ts slotlockDomainResolvers); the daily check runs
|
|
493
|
+
* in the server's maintenance. Off unless set.
|
|
494
|
+
*/
|
|
495
|
+
domains?: {
|
|
496
|
+
resolvers: readonly SlotlockDnsResolver[];
|
|
497
|
+
};
|
|
498
|
+
/**
|
|
499
|
+
* Single sign-on through each team's own identity provider (OpenID Connect; workspace-sso.ts,
|
|
500
|
+
* oidc.ts): needs verified domains. `secretBox` seals the teams' client secrets; `fetch` reaches
|
|
501
|
+
* the URLs a team's Owner gives (on Node, one that refuses private addresses). Off unless set.
|
|
502
|
+
*/
|
|
503
|
+
sso?: {
|
|
504
|
+
secretBox: SlotlockSecretBox;
|
|
505
|
+
fetch?: typeof fetch;
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* SCIM 2.0 provisioning (workspace-scim.ts, scim-server.ts): each team's identity provider adds,
|
|
509
|
+
* deactivates and groups its people at `<dashboard>/scim/v2/<team id>`, with a token an Owner
|
|
510
|
+
* makes on Settings, Identity. Needs verified domains. Off unless set.
|
|
511
|
+
*/
|
|
512
|
+
scim?: boolean;
|
|
513
|
+
/** Sends invitations by email; the person inviting is shown each link either way. */
|
|
514
|
+
invitations?: SlotlockInvitationSender;
|
|
376
515
|
keys: SlotlockApiKeyStore;
|
|
377
516
|
resources: SlotlockDashboardResources;
|
|
378
517
|
/** Shared by every server behind this origin: see SlotlockDashboardState. */
|
|
@@ -418,12 +557,17 @@ export interface SlotlockDashboard {
|
|
|
418
557
|
* The dashboard's resources, in the store under each tenant's row-level security. Adds hold the cap
|
|
419
558
|
* in the database (a READ COMMITTED transaction, under a per-tenant lock).
|
|
420
559
|
*/
|
|
421
|
-
export declare function createSlotlockDashboardResources(
|
|
560
|
+
export declare function createSlotlockDashboardResources(given: SlotlockStore, options?: {
|
|
422
561
|
/**
|
|
423
562
|
* The agent backend (`createSlotlockStoreAgentBackend`) a granted approval's booking is made
|
|
424
563
|
* through: with it, the dashboard lists approvals and decides them.
|
|
425
564
|
*/
|
|
426
565
|
agentBackend?: SlotlockAgentCalendarBackend;
|
|
566
|
+
/**
|
|
567
|
+
* Fetches holiday calendars by URL (the guarded fetch under `serve`): with it, Closures imports
|
|
568
|
+
* and subscribes to a calendar's address.
|
|
569
|
+
*/
|
|
570
|
+
holidayFetch?: typeof fetch;
|
|
427
571
|
}): SlotlockDashboardResources;
|
|
428
572
|
/**
|
|
429
573
|
* Records accepted write confirmations (`SlotlockAgentConfirmationOptions.consumeAcceptance`) in
|