@agentcairn/mcp-server 0.1.5 → 0.2.1
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/dist/authn.d.ts +7 -1
- package/dist/authn.js +34 -8
- package/dist/authn.js.map +1 -1
- package/dist/context.d.ts +9 -2
- package/dist/context.js +48 -10
- package/dist/context.js.map +1 -1
- package/dist/dashboard.js +305 -21
- package/dist/dashboard.js.map +1 -1
- package/dist/http.js +51 -9
- package/dist/http.js.map +1 -1
- package/dist/security.d.ts +2 -0
- package/dist/security.js +6 -0
- package/dist/security.js.map +1 -1
- package/dist/serve.js +3 -3
- package/dist/serve.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +81 -6
- package/dist/tools.js.map +1 -1
- package/package.json +2 -2
- package/ui/css/app.css +40 -2
- package/ui/js/app.js +18 -4
- package/ui/js/dom.js +39 -1
- package/ui/js/views/access.js +10 -4
- package/ui/js/views/audit.js +77 -0
- package/ui/js/views/auth.js +7 -8
- package/ui/js/views/graph.js +215 -54
- package/ui/js/views/invite.js +50 -0
- package/ui/js/views/project.js +162 -10
- package/ui/js/views/projects.js +8 -6
- package/ui/js/views/users.js +235 -0
package/ui/css/app.css
CHANGED
|
@@ -172,7 +172,7 @@ textarea { font-family: var(--mono); font-size: 13px; width: 100%; resize: verti
|
|
|
172
172
|
.badge svg { width: 11px; height: 11px; }
|
|
173
173
|
.kind { display: inline-flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--text-2); font-weight: 520; }
|
|
174
174
|
.kind::before { content: ""; width: 9px; height: 9px; border-radius: 3px; background: var(--k, var(--muted)); }
|
|
175
|
-
.k-overview { --k: var(--s1); } .k-decision { --k: var(--s2); } .k-convention { --k: var(--s3); } .k-gotcha { --k: var(--s4); }
|
|
175
|
+
.k-handoff { --k: var(--s8); } .k-overview { --k: var(--s1); } .k-decision { --k: var(--s2); } .k-convention { --k: var(--s3); } .k-gotcha { --k: var(--s4); }
|
|
176
176
|
.k-checkpoint { --k: var(--s5); } .k-thread { --k: var(--s6); } .k-glossary { --k: var(--s7); }
|
|
177
177
|
|
|
178
178
|
/* ===== data display ===== */
|
|
@@ -269,7 +269,7 @@ textarea { font-family: var(--mono); font-size: 13px; width: 100%; resize: verti
|
|
|
269
269
|
.graph-page { display: flex; flex-direction: column; height: calc(100vh - 56px); min-height: 560px; }
|
|
270
270
|
.graph-layout { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 16px; flex: 1; min-height: 0; }
|
|
271
271
|
.canvas-wrap { position: relative; min-height: 0; border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--border); background-color: var(--surface); box-shadow: var(--shadow-sm); }
|
|
272
|
-
#cy { position: absolute; inset: 0; background
|
|
272
|
+
#cy { position: absolute; inset: 0; background: var(--surface); }
|
|
273
273
|
.canvas-overlay { position: absolute; z-index: 5; }
|
|
274
274
|
.canvas-overlay.tl { left: 12px; top: 12px; right: 70px; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
275
275
|
.canvas-overlay.tl .chip { background: color-mix(in srgb, var(--surface) 88%, transparent); backdrop-filter: blur(6px); }
|
|
@@ -359,3 +359,41 @@ textarea { font-family: var(--mono); font-size: 13px; width: 100%; resize: verti
|
|
|
359
359
|
.page-head { margin-bottom: 16px; }
|
|
360
360
|
.toolbar { padding: 10px; }
|
|
361
361
|
}
|
|
362
|
+
|
|
363
|
+
/* handoff and sessions on the project page */
|
|
364
|
+
.notes { display: flex; flex-direction: column; gap: 6px; font-size: 13.5px; line-height: 1.5; }
|
|
365
|
+
.notes-h { margin: 10px 0 0; font-size: 11.5px; font-weight: 640; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); }
|
|
366
|
+
.notes-h:first-child { margin-top: 0; }
|
|
367
|
+
.notes-p { margin: 0; color: var(--text-2); }
|
|
368
|
+
.notes-list { margin: 0; padding-left: 20px; display: flex; flex-direction: column; gap: 3px; color: var(--text); }
|
|
369
|
+
.resume-card { border-left: 3px solid var(--s8); margin-bottom: 14px; }
|
|
370
|
+
.resume-card details { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 10px; }
|
|
371
|
+
.resume-card summary { cursor: pointer; font-weight: 560; }
|
|
372
|
+
.sessions { display: flex; flex-direction: column; margin-top: 4px; }
|
|
373
|
+
.session { padding: 12px 0; border-top: 1px solid var(--border); }
|
|
374
|
+
.session:first-child { border-top: 0; padding-top: 4px; }
|
|
375
|
+
.session summary { cursor: pointer; list-style: none; display: flex; flex-direction: column; gap: 4px; }
|
|
376
|
+
.session summary::-webkit-details-marker { display: none; }
|
|
377
|
+
.session .clamp { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; color: var(--text-2); font-size: 13px; }
|
|
378
|
+
.session .detail { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; font-size: 13px; }
|
|
379
|
+
.session .detail b { font-weight: 600; }
|
|
380
|
+
.session .files { font-family: var(--mono); font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
|
|
381
|
+
|
|
382
|
+
/* ---- users, sharing, scopes ---- */
|
|
383
|
+
.member { gap: 12px; padding: 4px 0; align-items: center; }
|
|
384
|
+
.member .hint { margin-top: 1px; }
|
|
385
|
+
.scope summary { cursor: pointer; color: var(--dim); font-size: 12.5px; }
|
|
386
|
+
.scope[open] summary { color: var(--text); }
|
|
387
|
+
.tbl .sub { display: block; font-size: 11.5px; color: var(--muted); margin-top: 2px; }
|
|
388
|
+
|
|
389
|
+
/* graph overview pills and properties, after Neo4j Browser */
|
|
390
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
391
|
+
.nlabel { display: inline-flex; align-items: center; gap: 6px; border: 0; border-radius: 999px; padding: 3px 10px; font: 600 12px var(--sans); cursor: pointer; }
|
|
392
|
+
.nlabel.static { cursor: default; }
|
|
393
|
+
.nlabel .n, .rlabel .n { font-weight: 500; opacity: 0.8; }
|
|
394
|
+
.nlabel[aria-pressed="true"] { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
395
|
+
.rlabel { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border-strong); border-radius: 4px; padding: 2px 8px; font: 11.5px var(--mono); color: var(--text-2); }
|
|
396
|
+
.props { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
|
397
|
+
.props th { text-align: left; font: 500 11.5px var(--mono); color: var(--muted); padding: 5px 10px 5px 0; vertical-align: top; white-space: nowrap; }
|
|
398
|
+
.props td { padding: 5px 0; overflow-wrap: anywhere; border-top: 1px solid var(--border); }
|
|
399
|
+
.props tr:first-child td { border-top: 0; }
|
package/ui/js/app.js
CHANGED
|
@@ -3,6 +3,7 @@ import { clear, h, icon, initial, startClock, svg } from './dom.js'
|
|
|
3
3
|
import { closeDrawer } from './drawer.js'
|
|
4
4
|
import { live } from './store.js'
|
|
5
5
|
import { mountLogin, mountSetup } from './views/auth.js'
|
|
6
|
+
import { mountInvite } from './views/invite.js'
|
|
6
7
|
|
|
7
8
|
const root = document.getElementById('root')
|
|
8
9
|
|
|
@@ -35,14 +36,20 @@ const ROUTES = [
|
|
|
35
36
|
{ pattern: /^\/graph$/, nav: 'graph', load: () => import('./views/graph.js') },
|
|
36
37
|
{ pattern: /^\/query$/, nav: 'query', load: () => import('./views/query.js') },
|
|
37
38
|
{ pattern: /^\/access$/, nav: 'access', load: () => import('./views/access.js') },
|
|
39
|
+
{ pattern: /^\/users$/, nav: 'users', load: () => import('./views/users.js') },
|
|
40
|
+
{ pattern: /^\/audit$/, nav: 'audit', load: () => import('./views/audit.js') },
|
|
38
41
|
{ pattern: /^\/system$/, nav: 'system', load: () => import('./views/system.js') },
|
|
39
42
|
]
|
|
40
43
|
|
|
41
44
|
const NAV = [
|
|
42
45
|
['Monitor', [['live', 'Live', 'live']]],
|
|
43
46
|
['Explore', [['projects', 'Projects', 'folder'], ['graph', 'Graph', 'graph'], ['query', 'Query', 'terminal']]],
|
|
44
|
-
['Manage', [['access', 'Access', 'key'], ['system', 'System', 'server']]],
|
|
47
|
+
['Manage', [['access', 'Access', 'key'], ['users', 'Users', 'users'], ['audit', 'Audit', 'list'], ['system', 'System', 'server']]],
|
|
45
48
|
]
|
|
49
|
+
/** Pages that show the whole server rather than your own projects: admins only. */
|
|
50
|
+
const ADMIN_ONLY = new Set(['live', 'query', 'system', 'users', 'audit'])
|
|
51
|
+
let me = { role: 'admin', solo: true }
|
|
52
|
+
|
|
46
53
|
const TITLES = Object.fromEntries(NAV.flatMap(([, items]) => items.map(([key, label]) => [key, label])))
|
|
47
54
|
|
|
48
55
|
export function navigate(hash) {
|
|
@@ -72,8 +79,9 @@ let shell = null
|
|
|
72
79
|
|
|
73
80
|
function mountShell(username, version) {
|
|
74
81
|
clear(root)
|
|
82
|
+
const visibleNav = NAV.map(([group, items]) => [group, items.filter(([key]) => me.role === 'admin' || !ADMIN_ONLY.has(key))]).filter(([, items]) => items.length)
|
|
75
83
|
const brand = h('div', { class: 'brand' }, logo(), 'Agent Cairn', version && h('small', null, `v${version}`))
|
|
76
|
-
const links =
|
|
84
|
+
const links = visibleNav.flatMap(([group, items]) => [
|
|
77
85
|
h('div', { class: 'label' }, group),
|
|
78
86
|
...items.map(([key, label, ic]) => {
|
|
79
87
|
const count = h('span', { class: 'count', dataset: { count: key } })
|
|
@@ -142,6 +150,7 @@ async function route() {
|
|
|
142
150
|
const { path, query } = parseHash()
|
|
143
151
|
const match = ROUTES.map(r => [r, r.pattern.exec(path)]).find(([, m]) => m)
|
|
144
152
|
const [def, m] = match ?? [ROUTES[0], null]
|
|
153
|
+
if (me.role !== 'admin' && ADMIN_ONLY.has(def.nav)) return navigate('#/projects')
|
|
145
154
|
const params = Object.fromEntries((def.params ?? []).map((name, i) => [name, decodeURIComponent(m?.[i + 1] ?? '')]))
|
|
146
155
|
|
|
147
156
|
document.title = `${TITLES[def.nav] ?? 'Dashboard'} · Agent Cairn`
|
|
@@ -161,7 +170,9 @@ async function route() {
|
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
172
|
|
|
164
|
-
|
|
173
|
+
const inviteToken = () => /^#\/invite\/([^/?]+)/.exec(location.hash)?.[1]
|
|
174
|
+
|
|
175
|
+
window.addEventListener('hashchange', () => (inviteToken() || !shell ? boot() : route()))
|
|
165
176
|
|
|
166
177
|
// ---- boot ------------------------------------------------------------------
|
|
167
178
|
|
|
@@ -171,6 +182,8 @@ async function boot() {
|
|
|
171
182
|
shell?.unsubscribe()
|
|
172
183
|
shell = null
|
|
173
184
|
live.stop()
|
|
185
|
+
const invite = inviteToken()
|
|
186
|
+
if (invite) return mountInvite(root, decodeURIComponent(invite), boot)
|
|
174
187
|
let state
|
|
175
188
|
try {
|
|
176
189
|
state = await api('state')
|
|
@@ -180,8 +193,9 @@ async function boot() {
|
|
|
180
193
|
}
|
|
181
194
|
if (state.setupRequired) return mountSetup(root, boot)
|
|
182
195
|
if (!state.authenticated) return mountLogin(root, boot)
|
|
196
|
+
me = { role: state.role ?? 'admin', solo: state.solo ?? true }
|
|
183
197
|
mountShell(state.username, state.version)
|
|
184
|
-
live.start()
|
|
198
|
+
if (me.role === 'admin') live.start()
|
|
185
199
|
route()
|
|
186
200
|
}
|
|
187
201
|
|
package/ui/js/dom.js
CHANGED
|
@@ -77,6 +77,8 @@ const ICONS = {
|
|
|
77
77
|
fit: '<path d="M4 9V5h4M20 9V5h-4M4 15v4h4M20 15v4h-4"/>',
|
|
78
78
|
info: '<circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 8h.01"/>',
|
|
79
79
|
arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>',
|
|
80
|
+
users: '<circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0M16 4.6a3.5 3.5 0 0 1 0 6.8M18 14.2a6.5 6.5 0 0 1 3.5 5.8"/>',
|
|
81
|
+
list: '<path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01"/>',
|
|
80
82
|
database: '<ellipse cx="12" cy="6" rx="8" ry="3"/><path d="M4 6v6c0 1.7 3.6 3 8 3s8-1.3 8-3V6M4 12v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"/>',
|
|
81
83
|
}
|
|
82
84
|
|
|
@@ -208,7 +210,43 @@ export function debounce(fn, ms) {
|
|
|
208
210
|
}
|
|
209
211
|
}
|
|
210
212
|
|
|
211
|
-
export const KINDS = ['overview', 'decision', 'convention', 'gotcha', 'checkpoint', 'thread', 'glossary']
|
|
213
|
+
export const KINDS = ['handoff', 'overview', 'decision', 'convention', 'gotcha', 'checkpoint', 'thread', 'glossary']
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Renders the small Markdown a handoff is written in (`## headings`, `- bullets`, `1. steps`) as elements.
|
|
217
|
+
* Text only goes in through textContent, never as HTML.
|
|
218
|
+
*/
|
|
219
|
+
export function renderNotes(body) {
|
|
220
|
+
const out = []
|
|
221
|
+
let list = null
|
|
222
|
+
const flush = () => {
|
|
223
|
+
if (list) out.push(list)
|
|
224
|
+
list = null
|
|
225
|
+
}
|
|
226
|
+
for (const raw of String(body).split('\n')) {
|
|
227
|
+
const line = raw.trimEnd()
|
|
228
|
+
if (!line.trim()) continue
|
|
229
|
+
const heading = /^##\s+(.*)$/.exec(line)
|
|
230
|
+
const bullet = /^-\s+(.*)$/.exec(line)
|
|
231
|
+
const step = /^\d+\.\s+(.*)$/.exec(line)
|
|
232
|
+
if (heading) {
|
|
233
|
+
flush()
|
|
234
|
+
out.push(h('h4', { class: 'notes-h' }, heading[1]))
|
|
235
|
+
} else if (bullet || step) {
|
|
236
|
+
const tag = step ? 'ol' : 'ul'
|
|
237
|
+
if (!list || list.tagName.toLowerCase() !== tag) {
|
|
238
|
+
flush()
|
|
239
|
+
list = h(tag, { class: 'notes-list' })
|
|
240
|
+
}
|
|
241
|
+
list.append(h('li', null, (bullet ?? step)[1]))
|
|
242
|
+
} else {
|
|
243
|
+
flush()
|
|
244
|
+
out.push(h('p', { class: 'notes-p' }, line))
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
flush()
|
|
248
|
+
return h('div', { class: 'notes' }, ...out)
|
|
249
|
+
}
|
|
212
250
|
|
|
213
251
|
export function kindLabel(kind) {
|
|
214
252
|
return h('span', { class: `kind k-${kind}` }, kind)
|
package/ui/js/views/access.js
CHANGED
|
@@ -13,14 +13,19 @@ export async function mount(container, { query }) {
|
|
|
13
13
|
|
|
14
14
|
// ---- tokens --------------------------------------------------------------
|
|
15
15
|
const nameInput = h('input', { class: 'input', placeholder: 'Name, e.g. laptop or ci-runner', maxlength: '60', 'aria-label': 'Token name', style: { width: '250px' } })
|
|
16
|
+
const readOnly = h('input', { type: 'checkbox', id: 'tok-ro' })
|
|
17
|
+
const scopeBoxes = projects.map(p => ({ p, box: h('input', { type: 'checkbox', value: p.id, 'aria-label': p.name }) }))
|
|
18
|
+
const scope = h('details', { class: 'scope' }, h('summary', null, 'Limit to projects'), h('div', { class: 'stack', style: { gap: '6px', padding: '8px 0' } }, scopeBoxes.length ? scopeBoxes.map(({ p, box }) => h('label', { class: 'row tight' }, box, p.name)) : h('span', { class: 'hint' }, 'No projects yet.'), h('span', { class: 'hint' }, 'None ticked means every project you can reach. A limited token cannot create projects.')))
|
|
16
19
|
const createBtn = h('button', { class: 'btn primary', type: 'button' }, icon('plus'), 'Create token')
|
|
17
20
|
|
|
18
21
|
async function create() {
|
|
19
22
|
createBtn.disabled = true
|
|
20
23
|
try {
|
|
21
|
-
const made = await api('tokens', { body: { name: nameInput.value } })
|
|
24
|
+
const made = await api('tokens', { body: { name: nameInput.value, readOnly: readOnly.checked, projects: scopeBoxes.filter(x => x.box.checked).map(x => x.p.id) } })
|
|
22
25
|
fresh = made.token
|
|
23
26
|
nameInput.value = ''
|
|
27
|
+
readOnly.checked = false
|
|
28
|
+
scopeBoxes.forEach(x => (x.box.checked = false))
|
|
24
29
|
tokens = (await api('tokens')).tokens
|
|
25
30
|
secretBox.replaceChildren(
|
|
26
31
|
h(
|
|
@@ -56,12 +61,13 @@ export async function mount(container, { query }) {
|
|
|
56
61
|
function drawTokens() {
|
|
57
62
|
const active = tokens.filter(t => !t.revokedAt)
|
|
58
63
|
tokenCard.replaceChildren(
|
|
59
|
-
h('div', { class: 'head' }, h('div', null, h('h2', null, 'Access tokens'), h('div', { class: 'hint' }, `${active.length} active · agents and the cairn CLI send one as a bearer token`)), h('div', { class: 'tools' }, nameInput, createBtn)),
|
|
64
|
+
h('div', { class: 'head' }, h('div', null, h('h2', null, 'Access tokens'), h('div', { class: 'hint' }, `${active.length} active · agents and the cairn CLI send one as a bearer token`)), h('div', { class: 'tools' }, nameInput, h('label', { class: 'row tight', for: 'tok-ro', title: 'The token can read memories and briefings but never write' }, readOnly, 'Read-only'), createBtn)),
|
|
65
|
+
h('div', { style: { padding: '0 18px 10px' } }, scope),
|
|
60
66
|
secretBox,
|
|
61
67
|
tokens.length
|
|
62
68
|
? h('div', { class: 'scroll', style: { maxHeight: '340px' } }, h('table', { class: 'tbl' }, h('thead', null, h('tr', null, ['Name', 'Token', 'Created', 'Last used', 'Status', ''].map(t => h('th', null, t)))),
|
|
63
69
|
h('tbody', null, tokens.map(t => h('tr', { style: { opacity: t.revokedAt ? 0.55 : 1 } },
|
|
64
|
-
h('td', null, h('span', { class: 'tool-name' }, t.name)),
|
|
70
|
+
h('td', null, h('span', { class: 'tool-name' }, t.name), t.readOnly && h('span', { class: 'badge', style: { marginLeft: '8px' } }, 'read-only'), t.projects?.length > 0 && h('span', { class: 'badge', style: { marginLeft: '6px' }, title: t.projects.map(id => projects.find(p => p.id === id)?.name ?? id).join(', ') }, `${t.projects.length} project${t.projects.length === 1 ? '' : 's'}`)),
|
|
65
71
|
h('td', { class: 'mono' }, `${t.prefix}…`),
|
|
66
72
|
h('td', null, ago(t.createdAt), t.createdBy && h('span', { class: 'sub' }, `by ${t.createdBy}`)),
|
|
67
73
|
h('td', null, t.lastUsedAt ? ago(t.lastUsedAt) : h('span', { class: 'muted' }, 'never')),
|
|
@@ -75,7 +81,7 @@ export async function mount(container, { query }) {
|
|
|
75
81
|
|
|
76
82
|
// ---- connect -------------------------------------------------------------
|
|
77
83
|
const origin = h('input', { class: 'input mono', value: location.origin, 'aria-label': 'Server address', style: { width: '280px' } })
|
|
78
|
-
const projectSelect = h('select', { 'aria-label': 'Project' }, projects.map(p => h('option', { value: p.id }, p.
|
|
84
|
+
const projectSelect = h('select', { 'aria-label': 'Project' }, projects.map(p => h('option', { value: p.id }, p.name)), h('option', { value: '' }, 'A project name or id…'))
|
|
79
85
|
const customProject = h('input', { class: 'input', placeholder: 'project-id', 'aria-label': 'Project id', style: { display: 'none', width: '160px' } })
|
|
80
86
|
if (query.project) projectSelect.value = query.project
|
|
81
87
|
else if (!projects.length) projectSelect.value = ''
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { api, qs } from '../api.js'
|
|
2
|
+
import { emptyState, fmt, h, toast } from '../dom.js'
|
|
3
|
+
|
|
4
|
+
const GROUPS = [
|
|
5
|
+
['', 'Everything'],
|
|
6
|
+
['login', 'Sign-ins'],
|
|
7
|
+
['user.', 'Accounts'],
|
|
8
|
+
['invite.', 'Invitations'],
|
|
9
|
+
['member.', 'Sharing'],
|
|
10
|
+
['project.', 'Projects'],
|
|
11
|
+
['token.', 'Tokens'],
|
|
12
|
+
['console.', 'Query console'],
|
|
13
|
+
]
|
|
14
|
+
const PAGE = 100
|
|
15
|
+
|
|
16
|
+
export async function mount(container) {
|
|
17
|
+
let events = []
|
|
18
|
+
let prefix = ''
|
|
19
|
+
let more = false
|
|
20
|
+
|
|
21
|
+
const body = h('tbody')
|
|
22
|
+
const table = h('div', { class: 'scroll' }, h('table', { class: 'tbl' }, h('thead', null, h('tr', null, ['When', 'Who', 'What', 'On', 'Detail'].map(t => h('th', null, t)))), body))
|
|
23
|
+
const holder = h('div', { class: 'card flush' })
|
|
24
|
+
const select = h('select', { 'aria-label': 'Kind of event' }, GROUPS.map(([v, label]) => h('option', { value: v }, label)))
|
|
25
|
+
const moreBtn = h('button', { class: 'btn', type: 'button' }, 'Load older')
|
|
26
|
+
|
|
27
|
+
function rows(list) {
|
|
28
|
+
return list.map(e =>
|
|
29
|
+
h(
|
|
30
|
+
'tr',
|
|
31
|
+
null,
|
|
32
|
+
h('td', { title: e.at }, h('span', { class: 'mono', style: { fontSize: '12px' } }, fmt.dateTime(e.at))),
|
|
33
|
+
h('td', null, e.actor ?? h('span', { class: 'muted' }, 'system')),
|
|
34
|
+
h('td', null, h('span', { class: `badge ${/failed|removed|disabled|revoked|deleted/.test(e.action) ? 'err' : /created|accepted|added|enabled/.test(e.action) ? 'ok' : ''}` }, e.action)),
|
|
35
|
+
h('td', null, e.target ?? ''),
|
|
36
|
+
h('td', { class: 'dim' }, e.detail ?? ''),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function load(append) {
|
|
42
|
+
moreBtn.disabled = true
|
|
43
|
+
try {
|
|
44
|
+
const { events: got } = await api(`audit?${qs({ limit: PAGE, action: prefix, before: append ? events[events.length - 1]?.at : undefined })}`)
|
|
45
|
+
events = append ? [...events, ...got] : got
|
|
46
|
+
more = got.length === PAGE
|
|
47
|
+
draw(append ? got : events, append)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
toast(error.message)
|
|
50
|
+
} finally {
|
|
51
|
+
moreBtn.disabled = false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function draw(list, append) {
|
|
56
|
+
if (!events.length) {
|
|
57
|
+
holder.replaceChildren(emptyState('inbox', 'Nothing recorded', prefix ? 'No events of this kind.' : 'Sign-ins and changes to accounts, sharing, tokens and projects show up here.'))
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
if (!holder.contains(table)) holder.replaceChildren(table, h('div', { class: 'row', style: { padding: '12px 18px', justifyContent: 'center' } }, moreBtn))
|
|
61
|
+
if (append) body.append(...rows(list))
|
|
62
|
+
else body.replaceChildren(...rows(list))
|
|
63
|
+
moreBtn.hidden = !more
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
select.addEventListener('change', () => {
|
|
67
|
+
prefix = select.value
|
|
68
|
+
load(false)
|
|
69
|
+
})
|
|
70
|
+
moreBtn.addEventListener('click', () => load(true))
|
|
71
|
+
|
|
72
|
+
container.append(
|
|
73
|
+
h('div', { class: 'page-head' }, h('div', null, h('h1', null, 'Audit log'), h('div', { class: 'sub' }, 'Who changed what, kept for 180 days. Secrets and memory content are never recorded.')), h('div', { class: 'row' }, select)),
|
|
74
|
+
holder,
|
|
75
|
+
)
|
|
76
|
+
await load(false)
|
|
77
|
+
}
|
package/ui/js/views/auth.js
CHANGED
|
@@ -1,33 +1,32 @@
|
|
|
1
1
|
import { api } from '../api.js'
|
|
2
2
|
import { clear, h, svg } from '../dom.js'
|
|
3
3
|
|
|
4
|
-
function brand() {
|
|
4
|
+
export function brand() {
|
|
5
5
|
return h(
|
|
6
6
|
'div',
|
|
7
7
|
{ class: 'brand' },
|
|
8
8
|
svg(
|
|
9
9
|
'svg',
|
|
10
10
|
{ viewBox: '0 0 32 32', 'aria-hidden': 'true' },
|
|
11
|
-
svg('
|
|
12
|
-
svg('
|
|
13
|
-
svg('
|
|
14
|
-
svg('circle', { cx: 24, cy: 21, r: 3, fill: 'var(--text)' }),
|
|
11
|
+
svg('ellipse', { cx: 16, cy: 23.5, rx: 9, ry: 3.8, fill: 'var(--text)' }),
|
|
12
|
+
svg('ellipse', { cx: 16, cy: 16.2, rx: 6.4, ry: 3.2, fill: 'var(--text)' }),
|
|
13
|
+
svg('ellipse', { cx: 16, cy: 9.6, rx: 3.8, ry: 2.8, fill: 'var(--s1)' }),
|
|
15
14
|
),
|
|
16
15
|
'Agent Cairn',
|
|
17
16
|
)
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
function field(label, input, hint) {
|
|
19
|
+
export function field(label, input, hint) {
|
|
21
20
|
const id = `f-${Math.random().toString(36).slice(2, 8)}`
|
|
22
21
|
input.id = id
|
|
23
22
|
return h('div', { class: 'field' }, h('label', { for: id }, label), input, hint && h('span', { class: 'hint' }, hint))
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
function page(...children) {
|
|
25
|
+
export function page(...children) {
|
|
27
26
|
return h('div', { class: 'auth-page' }, h('div', { class: 'card panel' }, brand(), ...children))
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
async function submit(form, button, errorEl, work) {
|
|
29
|
+
export async function submit(form, button, errorEl, work) {
|
|
31
30
|
errorEl.textContent = ''
|
|
32
31
|
button.disabled = true
|
|
33
32
|
try {
|