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.
- package/CHANGELOG.md +281 -0
- 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 +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +64 -9
- package/assistant.js +450 -82
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +21 -0
- package/dashboard-assistant-ui.js +539 -0
- package/dashboard-markdown.d.ts +2 -0
- package/dashboard-markdown.js +157 -0
- package/dashboard-settings-ui.d.ts +1 -0
- package/dashboard-settings-ui.js +78 -0
- package/dashboard.d.ts +84 -11
- package/dashboard.js +1654 -384
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -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/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- 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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
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 }`)
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
-
} |
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|