slotlock 0.7.0 → 0.9.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +281 -0
  2. package/agent-server.d.ts +25 -5
  3. package/agent-server.js +152 -7
  4. package/agent-store-backend.d.ts +6 -1
  5. package/agent-store-backend.js +861 -55
  6. package/application-api.d.ts +19 -1
  7. package/application-api.js +578 -21
  8. package/approvals.d.ts +2 -0
  9. package/approvals.js +12 -5
  10. package/assistant.d.ts +64 -9
  11. package/assistant.js +450 -82
  12. package/availability-store.d.ts +193 -0
  13. package/availability-store.js +558 -0
  14. package/booking-rules.d.ts +92 -5
  15. package/booking-rules.js +243 -15
  16. package/dashboard-assistant-ui.d.ts +21 -0
  17. package/dashboard-assistant-ui.js +539 -0
  18. package/dashboard-markdown.d.ts +2 -0
  19. package/dashboard-markdown.js +157 -0
  20. package/dashboard-settings-ui.d.ts +1 -0
  21. package/dashboard-settings-ui.js +78 -0
  22. package/dashboard.d.ts +84 -11
  23. package/dashboard.js +1654 -384
  24. package/ddl.d.ts +5 -1
  25. package/ddl.js +760 -9
  26. package/event-feed.d.ts +1 -1
  27. package/event-feed.js +18 -0
  28. package/holiday-import.d.ts +38 -0
  29. package/holiday-import.js +57 -0
  30. package/holidays.d.ts +38 -0
  31. package/holidays.js +150 -0
  32. package/hours.d.ts +125 -0
  33. package/hours.js +224 -0
  34. package/index.d.ts +14 -3
  35. package/index.js +10 -2
  36. package/interop.d.ts +25 -7
  37. package/interop.js +145 -17
  38. package/lifecycle.d.ts +31 -0
  39. package/lifecycle.js +115 -0
  40. package/maintenance.d.ts +19 -0
  41. package/maintenance.js +55 -1
  42. package/metrics.js +3 -0
  43. package/openapi.json +16237 -7795
  44. package/package.json +1 -1
  45. package/ranges.d.ts +28 -0
  46. package/ranges.js +135 -0
  47. package/resource-search.d.ts +19 -0
  48. package/resource-search.js +153 -0
  49. package/rest-api.js +7 -0
  50. package/rules.d.ts +2 -0
  51. package/rules.js +2 -0
  52. package/self-host.d.ts +1 -1
  53. package/self-host.js +221 -21
  54. package/slotlock.manifest.json +1 -1
  55. package/store.d.ts +367 -24
  56. package/store.js +1567 -97
  57. package/types.d.ts +198 -0
@@ -0,0 +1,157 @@
1
+ // The assistant's replies are Markdown. This renders the part of it a chat reply uses (paragraphs,
2
+ // headings, lists, quotes, rules, tables, code, bold, italic, strikethrough, http(s) links) as HTML,
3
+ // escaping everything first, so no input reaches the page as markup.
4
+ //
5
+ // It is one self-contained function on purpose: the dashboard's app.js embeds its source
6
+ // (Function.prototype.toString) to render a reply while it streams, and the server renders the
7
+ // stored reply with the very same function, so both always agree. Keep it free of imports and of
8
+ // anything outside its own body.
9
+ /** Safe HTML for a chat reply written in Markdown. */
10
+ export function renderSlotlockMarkdown(source) {
11
+ const text = String(source).replace(/\u0000/g, '').replace(/\r\n?/g, '\n');
12
+ const escape = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
13
+ const inline = (value) => {
14
+ const codes = [];
15
+ let out = value.replace(/`([^`\n]+)`/g, (_match, code) => {
16
+ codes.push(`<code>${escape(code)}</code>`);
17
+ return `\u0000${codes.length - 1}\u0000`;
18
+ });
19
+ const links = [];
20
+ out = out.replace(/\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, label, url) => {
21
+ links.push(`<a href="${escape(url)}" rel="noopener noreferrer">${escape(label)}</a>`);
22
+ return `\u0001${links.length - 1}\u0001`;
23
+ });
24
+ out = escape(out)
25
+ .replace(/\*\*(?=\S)([^\n]*?\S)\*\*/g, '<strong>$1</strong>')
26
+ .replace(/__(?=\S)([^\n]*?\S)__/g, '<strong>$1</strong>')
27
+ .replace(/~~(?=\S)([^\n]*?\S)~~/g, '<del>$1</del>')
28
+ .replace(/(^|[^*\w])\*(?=[^\s*])([^*\n]*?[^\s*])\*(?!\*)/g, '$1<em>$2</em>')
29
+ .replace(/(^|[^_\w])_(?=[^\s_])([^_\n]*?[^\s_])_(?![_\w])/g, '$1<em>$2</em>');
30
+ return out
31
+ .replace(/\u0001(\d+)\u0001/g, (_match, index) => {
32
+ const link = links[Number(index)] ?? '';
33
+ // A link's label keeps its own emphasis out: it was escaped whole.
34
+ return link;
35
+ })
36
+ .replace(/\u0000(\d+)\u0000/g, (_match, index) => codes[Number(index)] ?? '');
37
+ };
38
+ const cells = (line) => line
39
+ .trim()
40
+ .replace(/^\|/, '')
41
+ .replace(/\|$/, '')
42
+ .split('|')
43
+ .map((cell) => cell.trim());
44
+ const isRule = (line) => /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(line);
45
+ const listItem = /^(\s*)([-*+]|\d{1,9}[.)])\s+(.*)$/;
46
+ const lines = text.split('\n');
47
+ const blocks = [];
48
+ let paragraph = [];
49
+ const flush = () => {
50
+ if (paragraph.length > 0)
51
+ blocks.push(`<p>${paragraph.map(inline).join('<br>')}</p>`);
52
+ paragraph = [];
53
+ };
54
+ for (let i = 0; i < lines.length;) {
55
+ const line = lines[i] ?? '';
56
+ const fence = /^\s*(```|~~~)/.exec(line);
57
+ if (fence) {
58
+ flush();
59
+ const close = fence[1] ?? '```';
60
+ const body = [];
61
+ i += 1;
62
+ while (i < lines.length && !(lines[i] ?? '').trim().startsWith(close)) {
63
+ body.push(lines[i] ?? '');
64
+ i += 1;
65
+ }
66
+ i += 1;
67
+ blocks.push(`<pre><code>${escape(body.join('\n'))}</code></pre>`);
68
+ continue;
69
+ }
70
+ if (line.trim() === '') {
71
+ flush();
72
+ i += 1;
73
+ continue;
74
+ }
75
+ const heading = /^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
76
+ if (heading) {
77
+ flush();
78
+ blocks.push(`<h4>${inline(heading[2] ?? '')}</h4>`);
79
+ i += 1;
80
+ continue;
81
+ }
82
+ if (/^\s{0,3}([-*_])(\s*\1){2,}\s*$/.test(line) && !listItem.test(line.replace(/\s+$/, '') + ' x')) {
83
+ flush();
84
+ blocks.push('<hr>');
85
+ i += 1;
86
+ continue;
87
+ }
88
+ if (line.includes('|') && isRule(lines[i + 1] ?? '') && cells(line).length > 1) {
89
+ flush();
90
+ const head = cells(line);
91
+ const rows = [];
92
+ i += 2;
93
+ while (i < lines.length && (lines[i] ?? '').includes('|') && (lines[i] ?? '').trim() !== '') {
94
+ rows.push(cells(lines[i] ?? ''));
95
+ i += 1;
96
+ }
97
+ blocks.push(`<div class="md-table"><table><thead><tr>${head.map((cell) => `<th>${inline(cell)}</th>`).join('')}</tr></thead><tbody>${rows
98
+ .map((row) => `<tr>${head.map((_cell, index) => `<td>${inline(row[index] ?? '')}</td>`).join('')}</tr>`)
99
+ .join('')}</tbody></table></div>`);
100
+ continue;
101
+ }
102
+ if (/^\s{0,3}>/.test(line)) {
103
+ flush();
104
+ const quoted = [];
105
+ while (i < lines.length && /^\s{0,3}>/.test(lines[i] ?? '')) {
106
+ quoted.push((lines[i] ?? '').replace(/^\s{0,3}>\s?/, ''));
107
+ i += 1;
108
+ }
109
+ blocks.push(`<blockquote>${quoted.map(inline).join('<br>')}</blockquote>`);
110
+ continue;
111
+ }
112
+ if (listItem.test(line)) {
113
+ flush();
114
+ // Lists nest by indentation; each level is the kind (ordered or not) of its first item.
115
+ let html = '';
116
+ const open = [];
117
+ while (i < lines.length) {
118
+ const current = lines[i] ?? '';
119
+ const item = listItem.exec(current);
120
+ if (!item) {
121
+ // A wrapped line continues the item above it; a blank line or anything else ends the list.
122
+ if (current.trim() !== '' && /^\s+\S/.test(current) && open.length > 0) {
123
+ html += `<br>${inline(current.trim())}`;
124
+ i += 1;
125
+ continue;
126
+ }
127
+ break;
128
+ }
129
+ const indent = (item[1] ?? '').replace(/\t/g, ' ').length;
130
+ const tag = /\d/.test(item[2] ?? '') ? 'ol' : 'ul';
131
+ const top = open.at(-1);
132
+ if (!top || indent > top.indent + 1) {
133
+ if (top)
134
+ html = html.replace(/<\/li>$/, '');
135
+ html += `<${tag}>`;
136
+ open.push({ indent, tag });
137
+ }
138
+ else {
139
+ while (open.length > 1 && indent < (open.at(-1)?.indent ?? 0) - 1) {
140
+ html += `</li></${open.pop()?.tag}>`;
141
+ }
142
+ html += '</li>';
143
+ }
144
+ html += `<li>${inline(item[3] ?? '')}`;
145
+ i += 1;
146
+ }
147
+ while (open.length > 0)
148
+ html += `</li></${open.pop()?.tag}>`;
149
+ blocks.push(html);
150
+ continue;
151
+ }
152
+ paragraph.push(line.trim());
153
+ i += 1;
154
+ }
155
+ flush();
156
+ return blocks.join('');
157
+ }
@@ -0,0 +1 @@
1
+ export declare const SLOTLOCK_SETTINGS_CSS = "\n.settings { display: grid; grid-template-columns: 176px minmax(0, 640px); gap: var(--space-8); align-items: start; }\n.subnav { display: grid; gap: 2px; position: sticky; top: var(--space-6); }\n.subnav a { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-sm); color: var(--ink-2); font-size: 14px; text-decoration: none; }\n.subnav a:hover { background: var(--hover); color: var(--ink); }\n.subnav a[aria-current=\"page\"] { background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: inset 0 0 0 1px var(--line); }\n.subnav .nav-dot { width: 6px; height: 6px; border-radius: 50%; margin-left: auto; background: var(--good); }\n.settings-body { display: grid; gap: var(--space-6); min-width: 0; }\n.settings-body > .notice { margin: 0; }\n.card-head { margin: 0 0 calc(var(--space-2) - var(--space-6)); font-size: 13px; font-weight: 600; color: var(--ink-3); }\n.set-card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin: 0; }\n.row { display: grid; grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); gap: var(--space-4); align-items: center; min-height: 56px; padding: 12px 20px; border-top: 1px solid var(--line-soft); }\n.row.stack { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); align-items: start; padding-top: 16px; padding-bottom: 16px; }\n.row:first-child { border-top: 0; }\n.row-label { font-size: 14px; font-weight: 500; color: var(--ink); }\n.row-value { font-size: 14px; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }\n.row-value.muted { color: var(--ink-2); }\n.row input:not([type=\"radio\"]):not([type=\"checkbox\"]), .row select { width: 100%; }\n.mono { font-family: var(--mono); font-size: 13px; }\n.card-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 12px 20px; border-top: 1px solid var(--line); background: color-mix(in srgb, var(--surface) 70%, var(--canvas)); }\n.card-foot .fine { margin: 0; }\n.card-foot .primary { flex: none; }\n.status-head { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto auto; gap: var(--space-3); align-items: center; padding: 16px 20px; }\n.status-head form { margin: 0; }\n.mono-tile { display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--radius); background: var(--sunken); border: 1px solid var(--line); color: var(--ink); font-weight: 600; font-size: 14px; }\n.status-name { display: grid; gap: 2px; min-width: 0; }\n.status-name strong { font-size: 15px; font-weight: 600; color: var(--ink); }\n.status-name code { color: var(--ink-3); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: none; padding: 0; }\n.badge { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: var(--radius-full); font-size: 12px; font-weight: 500; color: var(--ink-2); background: var(--sunken); white-space: nowrap; }\n.badge::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: var(--ink-3); }\n.badge.ok { color: var(--good); background: color-mix(in srgb, var(--good) 14%, transparent); }\n.badge.ok::before { background: var(--good); }\n.badge.bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }\n.badge.bad::before { background: var(--bad); }\n.status-card .notice { margin: 0; border-radius: 0; border-width: 1px 0 0; box-shadow: none; font-size: 13px; padding: 10px 20px; }\n.status-card .notice.done { color: var(--ink); }\n.tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-2); }\n.tile { position: relative; display: flex; align-items: center; gap: var(--space-2); height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); font-size: 14px; color: var(--ink); cursor: pointer; transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease); }\n.tile:hover { background: var(--hover); }\n.tile input { position: absolute; opacity: 0; pointer-events: none; }\n.tile .tile-mark { display: grid; place-items: center; flex: none; width: 20px; height: 20px; border-radius: 5px; background: var(--sunken); color: var(--ink-2); font-size: 11px; font-weight: 600; }\n.tile:has(input:checked) { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }\n.tile:has(input:checked) .tile-mark { background: var(--accent); color: var(--surface); }\n.tile:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--ring); }\n.provider-form:not(:has(input[name=\"preset\"][value=\"custom\"]:checked)) .custom-only { display: none; }\n.segmented { justify-self: start; display: inline-flex; padding: 2px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--sunken); gap: 2px; }\n.segmented label { position: relative; display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 6px; font-size: 13px; color: var(--ink-2); cursor: pointer; }\n.segmented input { position: absolute; opacity: 0; pointer-events: none; }\n.segmented label:has(input:checked) { background: var(--surface); color: var(--ink); box-shadow: 0 0 0 1px var(--line); }\n.field-with-button { display: flex; gap: var(--space-2); align-items: center; }\n.field-with-button input { flex: 1; min-width: 0; }\n.field-with-button button { flex: none; }\n.key-set { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }\n.key-set code { background: none; padding: 0; color: var(--ink-2); letter-spacing: 0.04em; }\n.key-set details { display: contents; }\n.key-set details > summary { list-style: none; cursor: pointer; color: var(--accent); font-size: 13px; }\n.key-set details > summary::-webkit-details-marker { display: none; }\n.key-set details[open] > summary { display: none; }\n.key-set details[open] input { flex-basis: 100%; }\n.danger-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 14px 20px; }\n.danger-row strong { font-size: 14px; font-weight: 500; color: var(--ink); }\n.danger-row details.act > summary { color: var(--bad); }\n@media (max-width: 760px) {\n .settings { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }\n .subnav { position: static; display: flex; overflow-x: auto; gap: 0; border-bottom: 1px solid var(--line); scrollbar-width: none; }\n .subnav::-webkit-scrollbar { display: none; }\n .subnav a { height: 40px; border-radius: 0; flex: none; }\n .subnav a[aria-current=\"page\"] { background: none; box-shadow: inset 0 -2px 0 var(--accent); }\n .row { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); padding: 12px 16px; }\n .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .status-head { grid-template-columns: 32px minmax(0, 1fr) auto; padding: 14px 16px; }\n .status-head form { grid-column: 1 / -1; }\n .card-foot { flex-direction: column; align-items: stretch; padding: 12px 16px; }\n .card-foot .primary { height: 44px; }\n}\n";
@@ -0,0 +1,78 @@
1
+ // The dashboard Settings pages' styles: a section list beside one column of cards, label-left rows,
2
+ // provider tiles, and the assistant's status card. dashboard.ts renders the pages.
3
+ export const SLOTLOCK_SETTINGS_CSS = `
4
+ .settings { display: grid; grid-template-columns: 176px minmax(0, 640px); gap: var(--space-8); align-items: start; }
5
+ .subnav { display: grid; gap: 2px; position: sticky; top: var(--space-6); }
6
+ .subnav a { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-sm); color: var(--ink-2); font-size: 14px; text-decoration: none; }
7
+ .subnav a:hover { background: var(--hover); color: var(--ink); }
8
+ .subnav a[aria-current="page"] { background: var(--surface); color: var(--ink); font-weight: 500; box-shadow: inset 0 0 0 1px var(--line); }
9
+ .subnav .nav-dot { width: 6px; height: 6px; border-radius: 50%; margin-left: auto; background: var(--good); }
10
+ .settings-body { display: grid; gap: var(--space-6); min-width: 0; }
11
+ .settings-body > .notice { margin: 0; }
12
+ .card-head { margin: 0 0 calc(var(--space-2) - var(--space-6)); font-size: 13px; font-weight: 600; color: var(--ink-3); }
13
+ .set-card { background: var(--surface); border: 1px solid var(--line); border-radius: 10px; overflow: hidden; margin: 0; }
14
+ .row { display: grid; grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); gap: var(--space-4); align-items: center; min-height: 56px; padding: 12px 20px; border-top: 1px solid var(--line-soft); }
15
+ .row.stack { grid-template-columns: minmax(0, 1fr); gap: var(--space-3); align-items: start; padding-top: 16px; padding-bottom: 16px; }
16
+ .row:first-child { border-top: 0; }
17
+ .row-label { font-size: 14px; font-weight: 500; color: var(--ink); }
18
+ .row-value { font-size: 14px; color: var(--ink); min-width: 0; overflow-wrap: anywhere; }
19
+ .row-value.muted { color: var(--ink-2); }
20
+ .row input:not([type="radio"]):not([type="checkbox"]), .row select { width: 100%; }
21
+ .mono { font-family: var(--mono); font-size: 13px; }
22
+ .card-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 12px 20px; border-top: 1px solid var(--line); background: color-mix(in srgb, var(--surface) 70%, var(--canvas)); }
23
+ .card-foot .fine { margin: 0; }
24
+ .card-foot .primary { flex: none; }
25
+ .status-head { display: grid; grid-template-columns: 32px minmax(0, 1fr) auto auto; gap: var(--space-3); align-items: center; padding: 16px 20px; }
26
+ .status-head form { margin: 0; }
27
+ .mono-tile { display: grid; place-items: center; width: 32px; height: 32px; border-radius: var(--radius); background: var(--sunken); border: 1px solid var(--line); color: var(--ink); font-weight: 600; font-size: 14px; }
28
+ .status-name { display: grid; gap: 2px; min-width: 0; }
29
+ .status-name strong { font-size: 15px; font-weight: 600; color: var(--ink); }
30
+ .status-name code { color: var(--ink-3); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: none; padding: 0; }
31
+ .badge { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px; border-radius: var(--radius-full); font-size: 12px; font-weight: 500; color: var(--ink-2); background: var(--sunken); white-space: nowrap; }
32
+ .badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--ink-3); }
33
+ .badge.ok { color: var(--good); background: color-mix(in srgb, var(--good) 14%, transparent); }
34
+ .badge.ok::before { background: var(--good); }
35
+ .badge.bad { color: var(--bad); background: color-mix(in srgb, var(--bad) 14%, transparent); }
36
+ .badge.bad::before { background: var(--bad); }
37
+ .status-card .notice { margin: 0; border-radius: 0; border-width: 1px 0 0; box-shadow: none; font-size: 13px; padding: 10px 20px; }
38
+ .status-card .notice.done { color: var(--ink); }
39
+ .tiles { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-2); }
40
+ .tile { position: relative; display: flex; align-items: center; gap: var(--space-2); height: 40px; padding: 0 10px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); font-size: 14px; color: var(--ink); cursor: pointer; transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease); }
41
+ .tile:hover { background: var(--hover); }
42
+ .tile input { position: absolute; opacity: 0; pointer-events: none; }
43
+ .tile .tile-mark { display: grid; place-items: center; flex: none; width: 20px; height: 20px; border-radius: 5px; background: var(--sunken); color: var(--ink-2); font-size: 11px; font-weight: 600; }
44
+ .tile:has(input:checked) { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
45
+ .tile:has(input:checked) .tile-mark { background: var(--accent); color: var(--surface); }
46
+ .tile:has(input:focus-visible) { box-shadow: 0 0 0 3px var(--ring); }
47
+ .provider-form:not(:has(input[name="preset"][value="custom"]:checked)) .custom-only { display: none; }
48
+ .segmented { justify-self: start; display: inline-flex; padding: 2px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--sunken); gap: 2px; }
49
+ .segmented label { position: relative; display: inline-flex; align-items: center; height: 30px; padding: 0 12px; border-radius: 6px; font-size: 13px; color: var(--ink-2); cursor: pointer; }
50
+ .segmented input { position: absolute; opacity: 0; pointer-events: none; }
51
+ .segmented label:has(input:checked) { background: var(--surface); color: var(--ink); box-shadow: 0 0 0 1px var(--line); }
52
+ .field-with-button { display: flex; gap: var(--space-2); align-items: center; }
53
+ .field-with-button input { flex: 1; min-width: 0; }
54
+ .field-with-button button { flex: none; }
55
+ .key-set { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; }
56
+ .key-set code { background: none; padding: 0; color: var(--ink-2); letter-spacing: 0.04em; }
57
+ .key-set details { display: contents; }
58
+ .key-set details > summary { list-style: none; cursor: pointer; color: var(--accent); font-size: 13px; }
59
+ .key-set details > summary::-webkit-details-marker { display: none; }
60
+ .key-set details[open] > summary { display: none; }
61
+ .key-set details[open] input { flex-basis: 100%; }
62
+ .danger-row { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); padding: 14px 20px; }
63
+ .danger-row strong { font-size: 14px; font-weight: 500; color: var(--ink); }
64
+ .danger-row details.act > summary { color: var(--bad); }
65
+ @media (max-width: 760px) {
66
+ .settings { grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
67
+ .subnav { position: static; display: flex; overflow-x: auto; gap: 0; border-bottom: 1px solid var(--line); scrollbar-width: none; }
68
+ .subnav::-webkit-scrollbar { display: none; }
69
+ .subnav a { height: 40px; border-radius: 0; flex: none; }
70
+ .subnav a[aria-current="page"] { background: none; box-shadow: inset 0 -2px 0 var(--accent); }
71
+ .row { grid-template-columns: minmax(0, 1fr); gap: var(--space-2); padding: 12px 16px; }
72
+ .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
73
+ .status-head { grid-template-columns: 32px minmax(0, 1fr) auto; padding: 14px 16px; }
74
+ .status-head form { grid-column: 1 / -1; }
75
+ .card-foot { flex-direction: column; align-items: stretch; padding: 12px 16px; }
76
+ .card-foot .primary { height: 44px; }
77
+ }
78
+ `;
package/dashboard.d.ts CHANGED
@@ -4,13 +4,16 @@ import { type SlotlockAgentCalendarBackend, type SlotlockAgentOperation, type Sl
4
4
  import { type SlotlockAssistantStore } from './assistant.js';
5
5
  import { type SlotlockRate } from './config.js';
6
6
  import { type SlotlockBookingRules } from './booking-rules.js';
7
- import { type SlotlockTenantPlan } from './plans.js';
7
+ import { type SlotlockPlan, type SlotlockTenantPlan } from './plans.js';
8
8
  import { type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourceProfileInput } from './resource-profile.js';
9
9
  import { type SlotlockUsageRow } from './usage.js';
10
10
  import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
11
11
  import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
12
+ import type { SlotlockAvailabilityDefaults, SlotlockAvailabilityExceptionInput, SlotlockHolidayCalendar, SlotlockOverrideResources } from './availability-store.js';
13
+ import { type SlotlockAvailabilityException } from './hours.js';
14
+ import { type SlotlockHolidaySource } from './holiday-import.js';
12
15
  import { type SlotlockSandboxScenario } from './sandbox.js';
13
- import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
16
+ import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockContact, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
14
17
  /** Resources one dashboard tenant may hold. */
15
18
  export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
16
19
  /**
@@ -30,13 +33,51 @@ export interface SlotlockDashboardResources {
30
33
  */
31
34
  add(tenantRef: string, externalRef: string, timezone: string, actor?: string): Promise<SlotlockResource>;
32
35
  /**
33
- * Set the bookable hours of one of the tenant's resources (`{ id }`) or of all of them; `null`
34
- * hands them back to the server's and `[]` closes. Returns how many resources changed: 0 for an
35
- * id that is not the tenant's.
36
+ * Set the bookable hours of one of the tenant's resources (`{ id }`), one of its locations
37
+ * (`{ location }`), or the tenant's own (`'all'`), which every resource without its own inherits
38
+ * (RFC 0004 B2); `null` hands them to what is above and `[]` closes. For the tenant's, `override`
39
+ * says what becomes of resources' own hours (SlotlockOverrideResources: `none` keeps them).
40
+ * Returns 0 for an id or location that is not the tenant's, else at least 1 (for the tenant's, how
41
+ * many resources they now apply to).
36
42
  */
37
43
  setHours(tenantRef: string, target: {
38
44
  id: string;
39
- } | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string): Promise<number>;
45
+ } | {
46
+ location: string;
47
+ } | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string, override?: SlotlockOverrideResources): Promise<number>;
48
+ /** The people the tenant keeps to tell about bookings (RFC 0004 P3), and forgetting one. */
49
+ contacts?: {
50
+ list(tenantRef: string, limit: number, after?: string): Promise<SlotlockContact[]>;
51
+ forget(tenantRef: string, ref: string, actor?: string): Promise<{
52
+ deleted: boolean;
53
+ bookings: number;
54
+ }>;
55
+ };
56
+ /** The tenant's stored hours and its closures and special hours (RFC 0004 B1, B2). */
57
+ hours?: {
58
+ defaults(tenantRef: string): Promise<SlotlockAvailabilityDefaults>;
59
+ exceptions(tenantRef: string, limit: number): Promise<SlotlockAvailabilityException[]>;
60
+ addException(tenantRef: string, exception: Omit<SlotlockAvailabilityExceptionInput, 'tenantRef'>, actor?: string): Promise<SlotlockAvailabilityException>;
61
+ deleteException(tenantRef: string, id: string, actor?: string): Promise<boolean>;
62
+ /**
63
+ * Import a holiday calendar (its text, or a URL with `byUrl`): each all-day event closes its
64
+ * days for the next two years. Kept by name at its scope, so importing it again updates it and
65
+ * removes the holidays it dropped; `subscribe` imports a URL again every 30 days.
66
+ */
67
+ importHolidays(tenantRef: string, source: SlotlockHolidaySource, scope: {
68
+ location?: string;
69
+ resourceId?: string;
70
+ }, actor?: string): Promise<{
71
+ imported: number;
72
+ skipped: number;
73
+ removed: number;
74
+ }>;
75
+ /** Whether importHolidays reads calendars by URL. */
76
+ byUrl?: boolean;
77
+ /** The tenant's holiday calendars, and deleting one with its closures (false: not the tenant's). */
78
+ calendars?(tenantRef: string): Promise<SlotlockHolidayCalendar[]>;
79
+ deleteCalendar?(tenantRef: string, id: string, actor?: string): Promise<boolean>;
80
+ };
40
81
  /** What occupies the tenant's resources in `window`, by start: the overview's agenda. */
41
82
  agenda?(tenantRef: string, window: Interval, limit: number): Promise<SlotlockAgendaEntry[]>;
42
83
  /** The tenant's latest changes, newest first: the overview's activity. */
@@ -229,13 +270,38 @@ export interface SlotlockDashboardAssistantOptions {
229
270
  turnTimeoutMs?: number;
230
271
  /** Model calls one message may make (8 by default, at most 20). */
231
272
  maxSteps?: number;
232
- /** How long a conversation is kept after its last message: 2 hours by default. */
273
+ /**
274
+ * How long a conversation is kept after its last message (1 second to 3,650 days). Without it, a
275
+ * member keeps each conversation until they delete it, up to SLOTLOCK_ASSISTANT_MAX_CHATS.
276
+ */
233
277
  conversationTtlMs?: number;
234
278
  }
235
279
  /**
236
- * Hosted Slotlock's plans (plans.ts) and their billing: a Plan page with the tenant's plan and use,
280
+ * Hosted Slotlock's plans (plans.ts) and their billing: a Usage page with the tenant's plan and use,
237
281
  * Upgrade (to a checkout page) and Manage (to a billing portal). Off unless set.
238
282
  */
283
+ /**
284
+ * The tenant's plan and the deployment's default, read whether or not anything sells plans: with it
285
+ * the dashboard shows a Usage page (plan, limits and this month's use) and puts a tenant new to
286
+ * plans on the default before it adds what plans limit.
287
+ */
288
+ export interface SlotlockDashboardPlans {
289
+ /** The tenant's plan (its own, or the default once it applies), or null (the server's limits apply). */
290
+ plan(tenantRef: string): Promise<SlotlockTenantPlan | null>;
291
+ /** Puts a tenant new to plans on the default plan before it adds a key, resource or webhook endpoint. */
292
+ enrol?(tenantRef: string): Promise<void>;
293
+ /** The deployment's default plan and the date it applies to every tenant, or null. */
294
+ planDefault?(): Promise<{
295
+ plan: SlotlockPlan;
296
+ limits: SlotlockTenantPlan['limits'];
297
+ appliesFrom: Date;
298
+ } | null>;
299
+ /**
300
+ * Where asking for another plan goes (an https or mailto URL) when nothing sells plans here: with
301
+ * it, the Usage page offers Developer or Team by asking.
302
+ */
303
+ contact?: string;
304
+ }
239
305
  export interface SlotlockDashboardBilling {
240
306
  /** The tenant's plan (its own, or the default), or null when it has none (the server's limits apply). */
241
307
  plan(tenantRef: string): Promise<SlotlockTenantPlan | null>;
@@ -359,15 +425,17 @@ export interface SlotlockDashboardOptions {
359
425
  availability?: readonly WeeklyAvailabilityRule[];
360
426
  /** The chat assistant; off unless set. */
361
427
  assistant?: SlotlockDashboardAssistantOptions;
362
- /** Plans and billing (hosted Slotlock); off unless set. */
428
+ /** Plans (hosted Slotlock): a Usage page, and enrolment on the default plan. Off unless set. */
429
+ plans?: SlotlockDashboardPlans;
430
+ /** Paying for plans through Stripe (hosted Slotlock); off unless set. Implies `plans` from its own. */
363
431
  billing?: SlotlockDashboardBilling;
364
432
  /**
365
- * The deployment meters usage (SLOTLOCK_USAGE=on): the Plan page shows this month's, from the
433
+ * The deployment meters usage (SLOTLOCK_USAGE=on): the Usage page shows this month's, from the
366
434
  * resources adapter's `usage`.
367
435
  */
368
436
  usage?: boolean;
369
437
  /**
370
- * The deployment holds tenants to their plans' monthly quotas (SLOTLOCK_QUOTAS=on): the Plan page
438
+ * The deployment holds tenants to their plans' monthly quotas (SLOTLOCK_QUOTAS=on): the Usage page
371
439
  * shows this month's use against them, and warns from 80%. Needs `usage`.
372
440
  */
373
441
  quotas?: boolean;
@@ -400,6 +468,11 @@ export declare function createSlotlockDashboardResources(store: SlotlockStore, o
400
468
  * through: with it, the dashboard lists approvals and decides them.
401
469
  */
402
470
  agentBackend?: SlotlockAgentCalendarBackend;
471
+ /**
472
+ * Fetches holiday calendars by URL (the guarded fetch under `serve`): with it, Closures imports
473
+ * and subscribes to a calendar's address.
474
+ */
475
+ holidayFetch?: typeof fetch;
403
476
  }): SlotlockDashboardResources;
404
477
  /**
405
478
  * Records accepted write confirmations (`SlotlockAgentConfirmationOptions.consumeAcceptance`) in