pocolobby 0.12.2 → 0.14.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/dist/board-page.js +31 -28
- package/dist/board.js +1 -1
- package/dist/catalog.js +1 -1
- package/dist/cli.js +7 -7
- package/dist/console-page.js +110 -68
- package/dist/console.js +1 -1
- package/dist/pace.js +1 -1
- package/dist/protocol.js +1 -1
- package/dist/server.js +4 -4
- package/dist/statusline.js +6 -6
- package/dist/store.js +40 -11
- package/docs/API.md +105 -78
- package/docs/ARCHITECTURE.md +27 -27
- package/docs/LOCAL-SETUP.md +2 -2
- package/docs/OPERATIONS.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/examples/PACING.md +14 -24
- package/package.json +1 -1
package/dist/console-page.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var de=Object.defineProperty;var u=(s,e)=>de(s,"name",{value:e,configurable:!0});import{EMPTY_CSS as le,emptyState as T,mascot as G}from"./mascots.js";import{newestFirst as ce}from"./memory.js";import{ICON_LINKS as pe,iconVersion as K,lobbyIconLinks as me}from"./icons.js";import{displayName as q}from"./board.js";import{MODEL_NAMES as R,SUMMARY_EFFORTS as ue,SUMMARY_MODELS as he}from"./board-model.js";import{EARLY_SCRIPT as ge,escape as a,FONTS as be,MEMORY_RULE_CSS as fe,memoryLine as ye,HEADER_CSS as ve,THEME_SCRIPT as $e,THEME_SWITCH as we,topBar as xe,TOKENS as ke,WEEK_CSS as _e,weekBar as Se}from"./board-page.js";import{paceState as Ae,weeklyPace as Ce}from"./pace.js";import{DOLLARS as V,money as z,rateNote as B}from"./money.js";const Ne=[["overview","Overview"],["rooms","Rooms and repos"],["summaries","AI summaries"],["access","Dashboard access"],["agents","Agents"],["pacing","Pacing"],["tokens","Tokens"],["memory","Memory"],["activity","Activity"],["general","General"]],Te=`${ke}
|
|
2
2
|
/* Layout: the lobby owner's settings. A side list of sections on wide screens, tabs on a phone; each section is one job. Same tokens as the dashboard. */
|
|
3
3
|
html, body { margin: 0; }
|
|
4
4
|
.wrap { max-width: 1080px; margin: 0 auto; padding-block: 0 56px; padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); }
|
|
@@ -9,13 +9,13 @@ button, select, input { font: inherit; color: inherit; }
|
|
|
9
9
|
:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }
|
|
10
10
|
a { color: inherit; }
|
|
11
11
|
|
|
12
|
-
${
|
|
12
|
+
${ve}
|
|
13
13
|
/* Who is signed in, and Sign out, under every page. */
|
|
14
14
|
.foot { margin-top: 40px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; color: var(--muted); display: flex; gap: 8px 12px; align-items: center; flex-wrap: wrap; }
|
|
15
15
|
.foot form { margin: 0; }
|
|
16
16
|
.group > .seg { justify-self: start; }
|
|
17
17
|
/* The agents appear only where a page has a moment: empty lists, sign-in, a dead end. */
|
|
18
|
-
${
|
|
18
|
+
${le}
|
|
19
19
|
.welcome { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
|
|
20
20
|
.welcome .welcome-agent { width: 72px; margin: -8px 0 -6px; }
|
|
21
21
|
.welcome p { margin: 0; color: var(--muted); }
|
|
@@ -147,7 +147,7 @@ ul.memory form { flex: none; }
|
|
|
147
147
|
.mem-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; }
|
|
148
148
|
.mem-head h2 { margin: 0; font: 600 17px var(--display); }
|
|
149
149
|
.mem > .hint { margin: 0; }
|
|
150
|
-
${
|
|
150
|
+
${fe}
|
|
151
151
|
.mem-tools { display: grid; gap: 8px; justify-items: start; }
|
|
152
152
|
.mem-count { font: 600 12px var(--body); text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin-bottom: -4px; }
|
|
153
153
|
.mem [data-show-all] { justify-self: start; }
|
|
@@ -179,6 +179,35 @@ details.more > summary::after { content: "\u203A"; color: var(--muted); transiti
|
|
|
179
179
|
details.more[open] > summary::after { transform: rotate(90deg); }
|
|
180
180
|
details.more[open] { display: grid; gap: 10px; padding-bottom: 12px; }
|
|
181
181
|
ul.rows .main { min-width: 0; overflow-wrap: anywhere; }
|
|
182
|
+
/* Pacing: an agent's name, account and week at the left, its role at the right, centred on
|
|
183
|
+
the row, which is tinted by role as the dashboard tints its coordinator. */
|
|
184
|
+
ul.rows li.paced { flex-wrap: nowrap; }
|
|
185
|
+
ul.rows li.paced .main { flex: 1 1 auto; }
|
|
186
|
+
/* Its account and its week, a line each, cut short rather than wrapped. */
|
|
187
|
+
ul.rows li.paced .line { display: block; font-size: 13px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
188
|
+
ul.rows li.paced strong + .line { margin-top: 2px; }
|
|
189
|
+
ul.rows li.paced .pick { flex: none; margin-left: auto; text-align: right; }
|
|
190
|
+
ul.rows li.paced .pick form { margin: 0; }
|
|
191
|
+
ul.rows li.paced .pick .muted { white-space: nowrap; }
|
|
192
|
+
ul.rows li.paced select { width: 9.5rem; font-size: 15px; color: var(--ink); background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; }
|
|
193
|
+
ul.rows li.paced.worker { background: var(--sunk); }
|
|
194
|
+
ul.rows li.paced.worker select { background: var(--surface); }
|
|
195
|
+
ul.rows li.paced.coordinator { background: var(--brass-soft); }
|
|
196
|
+
ul.rows li.paced.worker .avatar, ul.rows li.paced.coordinator .avatar { background: var(--surface); }
|
|
197
|
+
/* Its account's week as the dashboard draws it, without the figures. */
|
|
198
|
+
${_e}
|
|
199
|
+
ul.rows li.paced .week { max-width: 280px; margin-top: 6px; }
|
|
200
|
+
ul.rows li.paced.worker .week .track, ul.rows li.paced.coordinator .week .track { fill: var(--surface); }
|
|
201
|
+
.card.clip { overflow: hidden; }
|
|
202
|
+
/* On a phone the lines wrap and the role narrows; it stays centred at the right. */
|
|
203
|
+
@media (max-width: 640px) {
|
|
204
|
+
ul.rows li.paced .line { white-space: normal; }
|
|
205
|
+
ul.rows li.paced select { width: 8.5rem; }
|
|
206
|
+
ul.rows li.paced .pick .muted { display: inline-block; max-width: 8.5rem; white-space: normal; }
|
|
207
|
+
}
|
|
208
|
+
/* The limits' Saving and Saved sit beside the fields, so nothing below them moves. */
|
|
209
|
+
.row-actions.limits { align-items: flex-end; }
|
|
210
|
+
.row-actions.limits .status { padding-bottom: 9px; }
|
|
182
211
|
.inline-rows li { padding-inline: 0; }
|
|
183
212
|
.avatar { display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 50%; background: var(--sunk); margin-right: 8px; font: 700 13px var(--display); color: var(--muted); vertical-align: middle; }
|
|
184
213
|
|
|
@@ -200,8 +229,8 @@ ul.rows .main { min-width: 0; overflow-wrap: anywhere; }
|
|
|
200
229
|
.source dt { margin-top: 6px; }
|
|
201
230
|
.panel-head h1 { font-size: 22px; }
|
|
202
231
|
}
|
|
203
|
-
`,
|
|
204
|
-
${
|
|
232
|
+
`,je=`
|
|
233
|
+
${$e}for (const form of document.querySelectorAll('form[data-action]')) {
|
|
205
234
|
const auto = 'auto' in form.dataset;
|
|
206
235
|
form.addEventListener('submit', async (event) => {
|
|
207
236
|
event.preventDefault();
|
|
@@ -316,53 +345,53 @@ for (const room of document.querySelectorAll('.mem')) {
|
|
|
316
345
|
apply();
|
|
317
346
|
});
|
|
318
347
|
}
|
|
319
|
-
`;function
|
|
348
|
+
`;function w(s,e,o){const r=s.signOut?`<footer class="foot"><span>${s.email?`Signed in as ${a(s.email)}`:"Signed in on this computer"}</span><form data-action="${a(s.signOut)}"><button class="linkish" type="submit">Sign out</button></form></footer>`:"",p=xe({home:s.home??"/",dashboard:s.lobby?.url??"",settings:s.lobby?.settings??null,lobbies:!!s.signOut},s.lobby?{name:s.lobby.name,current:"settings"}:null),l=s.lobby?`<div class="layout"><nav class="sections" aria-label="Sections">${Ne.map(([m,i])=>`<a href="${a(s.lobby.settings)}${m==="overview"?"":`/${m}`}"${m===s.section?' aria-current="page"':""}>${i}</a>`).join("")}</nav><div>${o}</div></div>`:`<div class="single">${o}</div>`;return`<!doctype html>
|
|
320
349
|
<html lang="en">
|
|
321
350
|
<head>
|
|
322
351
|
<meta charset="utf-8">
|
|
323
352
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
324
353
|
<meta name="referrer" content="same-origin">
|
|
325
354
|
<title>${a(e)}</title>
|
|
326
|
-
${s.lobby?.icon?
|
|
355
|
+
${s.lobby?.icon?me(K(s.lobby.icon),s.lobby.url.endsWith("/")?s.lobby.url:`${s.lobby.url}/`):pe}
|
|
327
356
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
328
357
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
329
|
-
<link rel="stylesheet" href="${
|
|
330
|
-
<style nonce="${s.nonce}">${
|
|
331
|
-
<script nonce="${s.nonce}">${
|
|
358
|
+
<link rel="stylesheet" href="${be}">
|
|
359
|
+
<style nonce="${s.nonce}">${Te}</style>
|
|
360
|
+
<script nonce="${s.nonce}">${ge}</script>
|
|
332
361
|
</head>
|
|
333
362
|
<body>
|
|
334
|
-
${
|
|
363
|
+
${p}
|
|
335
364
|
<main class="wrap">
|
|
336
365
|
<noscript><div class="note"><p>Settings need JavaScript for their buttons; the pages read without it.</p></div></noscript>
|
|
337
|
-
${
|
|
338
|
-
${
|
|
366
|
+
${l}
|
|
367
|
+
${r}
|
|
339
368
|
</main>
|
|
340
|
-
<script nonce="${s.nonce}">${
|
|
369
|
+
<script nonce="${s.nonce}">${je}</script>
|
|
341
370
|
</body>
|
|
342
371
|
</html>
|
|
343
|
-
`}
|
|
372
|
+
`}u(w,"page");const b='<p class="status" role="status" data-status></p>',x=u((s,e,o="")=>`<div class="panel-head"><div><h1>${a(s)}</h1><p>${a(e)}</p></div>${o}</div>`,"head"),E=u((s,e,o={},r="")=>`<form data-action="${a(s)}">${Object.entries(o).map(([p,l])=>`<input type="hidden" name="${p}" value="${a(l)}">`).join("")}<button class="btn${r?` ${r}`:""}" type="submit">${a(e)}</button>${b}</form>`,"buttonForm"),A=u(s=>s?new Date(s).toLocaleDateString("en-GB",{day:"numeric",month:"short",year:"numeric",timeZone:"UTC"}):"not recorded","date"),Ee=u(s=>`${new Date(s).toLocaleString("en-GB",{day:"numeric",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit",timeZone:"UTC"})} UTC`,"stamp"),S=u(s=>`${new Date(s).toLocaleString("en-GB",{day:"numeric",month:"short",hour:"2-digit",minute:"2-digit",timeZone:"UTC"})} UTC`,"time"),g=u((s,e,o=`${e}s`)=>`${s} ${s===1?e:o}`,"plural");function Ke(s,e,o,r){return w(s,`${e} on PocoLobby`,`<div class="card">${T("mouse-empty",a(e),a(o),r?`<a class="btn primary" href="${a(r.href)}">${a(r.label)}</a>`:"")}</div>`)}u(Ke,"renderNotice");function Ve(s,e){if(e.local)return w(s,"Sign in to PocoLobby",`${x("Sign in to PocoLobby","This broker runs on your computer, so signing in needs no email.")}
|
|
344
373
|
<div class="card pad signin">
|
|
345
|
-
<div class="welcome">${
|
|
374
|
+
<div class="welcome">${G("poco-bell","welcome-agent",72)}<p><b>Welcome to the lobby</b>Run <code>pocolobby settings --config owner.json</code> on this computer, with your owner file. It opens this page signed in.</p></div>
|
|
346
375
|
<form id="ticket-step" data-action="/api/sign-in/ticket" data-busy="Signing in\u2026">
|
|
347
376
|
<input type="hidden" name="ticket" value="">
|
|
348
|
-
${
|
|
377
|
+
${b}
|
|
349
378
|
</form>
|
|
350
|
-
</div>`);const o=e.lobby?`Sign in to open ${e.lobby}.`:"We send a six-digit code to your email. No password to remember.";if(e.missing.length)return
|
|
379
|
+
</div>`);const o=e.lobby?`Sign in to open ${e.lobby}.`:"We send a six-digit code to your email. No password to remember.";if(e.missing.length)return w(s,"Sign in to PocoLobby",`${x("Sign in to PocoLobby",o)}<div class="card pad signin"><p>Sign-in is not configured on this server yet: ${a(e.missing.join(" and "))} ${e.missing.length===1?"is":"are"} not set. Agents are not affected.</p></div>`);const r=e.lobby?`<input type="hidden" name="lobby" value="${a(e.lobby)}">${e.to&&e.to!=="/"?`<input type="hidden" name="to" value="${a(e.to)}">`:""}${e.state?`<input type="hidden" name="state" value="${a(e.state)}">`:""}`:"";return w(s,"Sign in to PocoLobby",`${x("Sign in to PocoLobby",o)}
|
|
351
380
|
<div class="card pad signin">
|
|
352
|
-
<div class="welcome">${
|
|
381
|
+
<div class="welcome">${G("poco-bell","welcome-agent",72)}<p><b>Welcome to the lobby</b>Poco, at the front desk, sends your code.</p></div>
|
|
353
382
|
<form data-action="/api/sign-in/code" data-reveal="code-step" data-busy="Sending\u2026">
|
|
354
383
|
<label class="field" for="email">Email<input id="email" name="email" type="email" autocomplete="email" required maxlength="254" autofocus></label>
|
|
355
384
|
<div class="row-actions"><button class="btn primary" type="submit">Send code</button></div>
|
|
356
|
-
${
|
|
385
|
+
${b}
|
|
357
386
|
</form>
|
|
358
387
|
<form id="code-step" data-action="/api/sign-in/verify" data-busy="Checking\u2026" hidden>
|
|
359
|
-
<input type="hidden" name="email" value="">${
|
|
388
|
+
<input type="hidden" name="email" value="">${r}
|
|
360
389
|
<label class="field" for="code">Code from the email<input id="code" name="code" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" required placeholder="123456"></label>
|
|
361
390
|
<p class="hint">The code works once, for 10 minutes. After 5 wrong tries, ask for a new one.</p>
|
|
362
391
|
<div class="row-actions"><button class="btn primary" type="submit">Sign in</button></div>
|
|
363
|
-
${
|
|
392
|
+
${b}
|
|
364
393
|
</form>
|
|
365
|
-
</div>`)}
|
|
394
|
+
</div>`)}u(Ve,"renderSignIn");function Je(s,e){const o=e.mayCreate&&(e.remaining===null||e.remaining>0)?`<details class="toggle adder"><summary class="btn primary">Create lobby</summary>
|
|
366
395
|
<form class="card pad" data-action="/api/lobbies" data-busy="Creating\u2026">
|
|
367
396
|
<div class="fields">
|
|
368
397
|
<label class="field" for="nl-name">Lobby name<input id="nl-name" name="lobby_id" required maxlength="48" pattern="[a-z]([a-z0-9-]*[a-z0-9])?" placeholder="my-team"></label>
|
|
@@ -370,21 +399,21 @@ ${f}
|
|
|
370
399
|
</div>
|
|
371
400
|
<p class="hint">Lowercase letters, digits and inner dashes, starting with a letter. Its dashboard lives at ${a(e.address("name"))}. Names such as www, api, settings and v1 are reserved, and a deleted lobby's name is never used again.${e.remaining===null?"":` You can create ${g(e.remaining,"more lobby","more lobbies")}.`}</p>
|
|
372
401
|
<div class="row-actions"><button class="btn primary" type="submit">Create lobby</button></div>
|
|
373
|
-
${
|
|
374
|
-
</form></details>`:"",
|
|
375
|
-
${
|
|
402
|
+
${b}
|
|
403
|
+
</form></details>`:"",r=e.owned.length?e.owned.map(l=>`<article class="card pad room"><header><h2>${a(l.display_name||l.lobby_id)}</h2><span class="chip ${l.agents?"c-ok":"c-off"}">${g(l.agents,"agent")} and ${g(l.rooms,"room")}</span></header><div class="members">${a(e.address(l.lobby_id))}, ${l.visibility} dashboard, created ${a(A(l.created_at))}</div><div class="row-actions"><a class="btn" href="${a(e.open(l.lobby_id))}">Open</a></div></article>`).join(""):`<div class="card">${T("gonk-server","No lobbies yet",e.mayCreate?"Create one below. Each lobby gets its own agents, rooms and dashboard.":"The server operator lets you create lobbies; ask them, or open a dashboard shared with you.")}</div>`,p=e.viewing.length?`<h2 class="subhead">Dashboards shared with you</h2><div class="card"><ul class="rows">${e.viewing.map(l=>`<li><span class="main"><strong>${a(l.display_name||l.lobby_id)}</strong> <span class="muted">dashboard shared with you</span></span><a class="btn" href="${a(l.url)}">Open dashboard</a></li>`).join("")}</ul></div>`:"";return w(s,"Your lobbies on PocoLobby",`${x("Your lobbies","Lobbies you own. Each one has its own agents, rooms and dashboard.")}<div class="stack">${r}${o}${p}</div>`)}u(Je,"renderHome");function Ze(s,e){const o=s.lobby,r=e.config.sources.length,p=e.view.sources.filter(n=>n.commit!==null&&!n.error).length,l=e.view.sources.filter(n=>n.error),m=e.rooms.length?l.length?`<span class="chip c-crit">${g(l.length,"source")} failing</span>`:r<e.rooms.length?`<span class="chip c-warn">${r} of ${g(e.rooms.length,"room")} connected</span>`:`<span class="chip c-ok">All ${g(e.rooms.length,"room")} connected</span>`:'<span class="chip c-off">No rooms yet</span>',i=e.last?e.last.error?'<span class="chip c-crit">Last run failed</span>':'<span class="chip c-ok">Up to date</span>':'<span class="chip c-off">No summary yet</span>',d=e.agents.filter(n=>n.enabled),c=d.length?"":`<div class="card">${T("mouse-envelope","No agents yet",`The server operator admits each agent with <span class="mono">pocolobby agent add NAME --lobby ${a(o.id)}</span> and hands it its credential; it shows here once it connects.`)}</div>`;return w(s,`${o.name} in PocoLobby settings`,`${x("Overview",`${g(d.length,"agent")} and ${g(e.rooms.length,"room")}.`)}
|
|
404
|
+
${c}
|
|
376
405
|
<div class="health">
|
|
377
|
-
<div class="card"><h3>Repositories</h3>${
|
|
378
|
-
<div class="card"><h3>Summaries</h3>${i}<p>${a(
|
|
406
|
+
<div class="card"><h3>Repositories</h3>${m}<p>${e.rooms.length?`${p} of ${g(r,"source")} read${l.length?`; ${a(l.map(n=>n.room_id).join(", "))} failing`:""}.`:"Add rooms, then pick a repository for each."}</p></div>
|
|
407
|
+
<div class="card"><h3>Summaries</h3>${i}<p>${a(R[e.config.summary_model])}, ${e.config.summary_effort} reasoning.${e.last?` Last run ${a(S(e.last.created_at))}.`:""}</p></div>
|
|
379
408
|
<div class="card"><h3>Dashboard</h3><span class="chip ${e.config.visibility==="public"?"c-warn":"c-ok"}">${e.config.visibility==="public"?"Public":"Private"}</span><p>${a(o.address)}, ${g(e.viewers,"viewer")} and ${g(e.sessions,"session")} open</p></div>
|
|
380
|
-
</div>`)}
|
|
381
|
-
<form class="card pad" data-action="${
|
|
409
|
+
</div>`)}u(Ze,"renderOverview");function Xe(s,e){const o=s.lobby.id,r=`${s.lobby.api}/rooms`,p=e.agents.filter(n=>n.enabled).map(n=>n.agent_id),l=`<details class="toggle adder"><summary class="btn primary">Add room</summary>
|
|
410
|
+
<form class="card pad" data-action="${r}" data-busy="Creating\u2026">
|
|
382
411
|
<div class="fields"><label class="field" for="nr-name">Room name<input id="nr-name" name="room_id" required maxlength="48" pattern="[a-z][a-z0-9_-]*" placeholder="for example 3d"></label></div>
|
|
383
412
|
<p class="hint">Members and a repository are added after the room exists. A coordinator can also create rooms from its tools; both show in Activity.</p>
|
|
384
413
|
<div class="row-actions"><button class="btn primary" type="submit">Create room</button></div>
|
|
385
|
-
${
|
|
386
|
-
</form></details>`,
|
|
387
|
-
<form class="edit" data-action="${
|
|
414
|
+
${b}
|
|
415
|
+
</form></details>`,m=`<div class="card pad"><div class="keyline"><strong>GitHub</strong><span class="muted">${e.github?"read-only token on the server":"no token on the server: notes are not read"}, ${g(e.repos.length,"repository","repositories")} approved for this lobby</span><button class="btn" disabled title="For customer lobbies, later">Connect GitHub</button></div>${e.repos.length?"":'<p class="hint">The server operator approves repositories for a lobby (pocolobby repos).</p>'}</div>`,i=u(n=>e.repos.map(h=>`<option value="${a(h)}"${n?.toLowerCase()===h?" selected":""}>${a(h)}</option>`).join(""),"repoOptions"),d=e.rooms.map(n=>{const h=e.config.sources.find(f=>f.room_id===n.room_id)??null,k=e.view.sources.find(f=>f.room_id===n.room_id),v=e.snapshots.get(n.room_id),y=h?k?.error?'<span class="chip c-crit">Failing</span>':v?`<span class="chip c-ok">Read ${a(k?.fetched??"")} ago at ${a(v.commit_sha.slice(0,7))}</span>`:'<span class="chip c-warn">Not read yet</span>':'<span class="chip c-off">No repository</span>',L=u(f=>`<span class="member">${a(f)}<form data-action="${r}/${encodeURIComponent(n.room_id)}/members"><input type="hidden" name="remove" value="${a(f)}"><button type="submit" aria-label="Remove ${a(f)} from ${a(n.room_id)}" title="Remove from room">\xD7</button></form></span>`,"member"),j=p.filter(f=>!n.members.includes(f)),P=`<div class="members">${n.members.length?n.members.map(L).join(""):"<span>No members</span>"}${j.length?`<form class="row-actions" data-action="${r}/${encodeURIComponent(n.room_id)}/members"><select name="add" aria-label="Add a member to ${a(n.room_id)}" class="btn"><option value="">Add member\u2026</option>${j.map(f=>`<option value="${a(f)}">${a(f)}</option>`).join("")}</select><button class="btn" type="submit">Add</button>${b}</form>`:""}</div>`,D=v?v.files.length?`<div class="files-line">${g(v.files.length,"file")}: ${a(v.files.map(f=>f.path.split("/").pop()).join(", "))}${k?.cut.length?`; only the beginning of ${a(k.cut.join(", "))}`:""}</div>`:'<div class="files-line">No notes found at these paths.</div>':"",N=h?`<dl class="source"><dt>Repository</dt><dd class="mono">${a(h.repo)}</dd><dt>Branch</dt><dd class="mono">${a(h.ref)}</dd><dt>Notes</dt><dd><div class="files">${h.notes.map(f=>`<span class="file">${a(f)}</span>`).join("")}</div>${D}</dd>${k?.error?`<dt>Problem</dt><dd>${a(k.error)}</dd>`:""}</dl>`:'<div class="empty-source">This room reads no notes yet. Pick a repository and the folders or files the dashboard should read.</div>',O=e.repos.length?`<details class="toggle"><summary class="btn">${h?"Edit source":"Choose repository"}</summary>
|
|
416
|
+
<form class="edit" data-action="${r}/${encodeURIComponent(n.room_id)}/source" data-busy="Applying\u2026">
|
|
388
417
|
<div class="fields">
|
|
389
418
|
<label class="field">Repository<select name="repo" required>${i(h?.repo??null)}</select></label>
|
|
390
419
|
<label class="field">Branch<input name="ref" value="${a(h?.ref??"main")}" maxlength="200"></label>
|
|
@@ -392,76 +421,89 @@ ${f}
|
|
|
392
421
|
</div>
|
|
393
422
|
<p class="hint">Separate paths with commas. A folder reads every Markdown file directly inside it. The dashboard reads them at one commit within a minute of applying.</p>
|
|
394
423
|
<div class="row-actions"><button class="btn primary" type="submit">Apply source</button></div>
|
|
395
|
-
${
|
|
396
|
-
</form>${h?`<div class="edit">${
|
|
424
|
+
${b}
|
|
425
|
+
</form>${h?`<div class="edit">${E(`${r}/${encodeURIComponent(n.room_id)}/source/remove`,"Stop reading notes")}</div>`:""}</details>`:"",I=`<details class="toggle"><summary class="btn">Archive room</summary><div class="edit"><p><strong>Archive #${a(n.room_id)}?</strong> No new messages can be sent to it and its members are removed. Messages sent before now are still delivered, scheduled ones included, and the history stays readable. Its daily schedules stop. You can restore it later, empty.</p>${E(`${r}/${encodeURIComponent(n.room_id)}/archive`,`Archive #${n.room_id}`,{},"danger")}</div></details>`;return`<article class="card pad room"><header><h2><span class="hash">#</span>${a(n.room_id)}</h2>${y}</header>${P}${N}<div class="row-actions">${O}${I}</div></article>`}),c=e.archived.length?`<div class="card"><ul class="rows">${e.archived.map(n=>`<li><span class="main"><strong>#${a(n.room_id)}</strong> <span class="muted">archived ${a(S(n.archived_at))}</span></span>${E(`${r}/${encodeURIComponent(n.room_id)}/restore`,"Restore")}</li>`).join("")}</ul></div>`:"";return w(s,`Rooms and repos for ${s.lobby.name}`,`${x("Rooms and repos","Each room follows one repository, at one or more paths. The dashboard reads them for what each agent does now and next.")}<div class="stack">${l}${m}${d.join("")||`<div class="card">${T("mouse-empty","No rooms yet","Add one above. A room follows one repository, and its agents talk there.")}</div>`}${c?`<h2 class="subhead">Archived rooms</h2>${c}`:""}</div>`)}u(Xe,"renderRooms");const J={"claude-haiku-5-5":{text:"Recommended. Fast and enough for summaries.",price:"$0.10 in and $0.50 out per million tokens"},"claude-sonnet-5-5":{text:"Better judgment on messy notes.",price:"$2 in and $10 out per million tokens"}},Le=u(s=>s>=1e9?`${(s/1e9).toFixed(1)} B`:s>=1e6?`${(s/1e6).toFixed(1)} M`:s>=1e3?`${Math.round(s/1e3)} K`:String(s),"tokens");function Qe(s,e){const o=s.lobby.id,r=`${s.lobby.api}/summaries`,p=he.map(i=>`<label class="choice"><input type="radio" name="model" value="${i}"${e.config.summary_model===i?" checked":""}><strong>${R[i]}</strong><span>${J[i].text}</span><span class="price">${J[i].price}</span></label>`).join(""),l=ue.map(i=>`<label><input type="radio" name="effort" value="${i}"${e.config.summary_effort===i?" checked":""}>${i[0].toUpperCase()}${i.slice(1)}</label>`).join(""),m=e.last?e.last.error?`<p class="warning">The last run, ${a(S(e.last.created_at))} with ${a(R[e.last.model]??e.last.model)}, failed: ${a(e.last.error)}.${e.good?` The dashboard shows the summary from ${a(S(e.good.created_at))}.`:""}</p>`:`<p class="hint">Last summary ${a(S(e.last.created_at))} by ${a(R[e.last.model]??e.last.model)}, ${z(e.last.cost_usd,s.lobby?.money??V,4)}.</p>`:'<p class="hint">No summary yet. One is written once the notes have been read.</p>';return w(s,`AI summaries for ${s.lobby.name}`,`${x("AI summaries","A small model reads the rooms\u2019 notes and the agents\u2019 reports and writes what each agent does now and next. It only runs when something changed, at most every 10 minutes.")}
|
|
397
426
|
<div class="stack"><div class="card pad">
|
|
398
|
-
<form class="stack" data-action="${
|
|
427
|
+
<form class="stack" data-action="${r}" data-busy="Saving\u2026" data-auto data-stay>
|
|
399
428
|
<div class="group"><span class="label">Provider</span><div class="choices"><label class="choice"><input type="radio" checked disabled><strong>Anthropic</strong><span>Claude models</span></label><div class="choice off" aria-disabled="true"><strong>OpenAI</strong><span>Added once tested</span></div></div></div>
|
|
400
429
|
<div class="group"><span class="label">Key</span><div class="keyline">${e.keySet?'<span class="chip c-ok">Set on the server</span><span class="muted">ANTHROPIC_API_KEY is configured on the server.</span>':'<span class="chip c-crit">Not set</span><span class="muted">The server has no ANTHROPIC_API_KEY, so no new summaries are written.</span>'}<button class="btn" type="button" disabled title="For customer lobbies, later">Use a lobby key</button></div></div>
|
|
401
|
-
<div class="group"><span class="label">Model</span><div class="choices" role="radiogroup" aria-label="Model">${
|
|
402
|
-
<div class="group"><span class="label">Reasoning</span><div class="seg" role="radiogroup" aria-label="Reasoning">${
|
|
403
|
-
${
|
|
430
|
+
<div class="group"><span class="label">Model</span><div class="choices" role="radiogroup" aria-label="Model">${p}</div></div>
|
|
431
|
+
<div class="group"><span class="label">Reasoning</span><div class="seg" role="radiogroup" aria-label="Reasoning">${l}</div><p class="hint">Low is plenty for summarizing. Higher levels think longer and cost more per run.</p></div>
|
|
432
|
+
${b}
|
|
404
433
|
</form>
|
|
405
434
|
</div>
|
|
406
435
|
<div class="card pad">
|
|
407
436
|
<div class="group"><span class="label">Last 24 hours</span><div class="usage">
|
|
408
437
|
<div class="stat"><div class="n">${e.usage.runs-e.usage.failed}</div><div class="k">summaries written${e.usage.failed?`, ${e.usage.failed} failed`:""}</div></div>
|
|
409
|
-
<div class="stat"><div class="n">${
|
|
410
|
-
<div class="stat"><div class="n">${
|
|
411
|
-
</div>${s.lobby?.money.date?`<p class="hint">${a(
|
|
412
|
-
${
|
|
413
|
-
<form data-action="${
|
|
438
|
+
<div class="stat"><div class="n">${Le(e.usage.input_tokens)}</div><div class="k">tokens read</div></div>
|
|
439
|
+
<div class="stat"><div class="n">${z(e.usage.cost_usd,s.lobby?.money??V,e.usage.cost_usd<.1?3:2)}</div><div class="k">estimated cost</div></div>
|
|
440
|
+
</div>${s.lobby?.money.date?`<p class="hint">${a(B(s.lobby.money).trim())}</p>`:""}</div>
|
|
441
|
+
${m}
|
|
442
|
+
<form data-action="${r}/test" data-busy="Starting\u2026" data-stay><div class="row-actions"><button class="btn" type="submit"${e.keySet?"":" disabled"}>Run a test summary</button></div>${b}</form>
|
|
414
443
|
<p class="hint">A test runs now with the settings above, at most once every 5 minutes, and costs like any run. Reload to see its result.</p>
|
|
415
|
-
</div></div>`)}
|
|
444
|
+
</div></div>`)}u(Qe,"renderSummaries");function ea(s,e){const o=s.lobby,r=`${o.api}/access`,p=u((m,i,d)=>`<label class="choice"><input type="radio" name="visibility" value="${m}"${e.config.visibility===m?" checked":""}><strong>${i}</strong><span>${d}</span></label>`,"option"),l=e.viewers.length?`<ul class="rows inline-rows">${e.viewers.map(m=>`<li><span class="main">${a(m.email)} <span class="muted">${m.disabled?"disabled by the operator":m.sessions?`signed in on ${g(m.sessions,"device")}`:"not signed in"}</span></span>${E(`${r}/viewers/remove`,"Remove",{email:m.email})}</li>`).join("")}</ul>`:'<p class="hint">No viewers. Only you open this dashboard while it is private.</p>';return w(s,`Dashboard access for ${o.name}`,`${x("Dashboard access","Who can open this lobby\u2019s dashboard.")}
|
|
416
445
|
<div class="stack"><div class="card pad">
|
|
417
446
|
<div class="group"><span class="label">Address</span><div class="addr"><code>${a(o.address)}</code><button class="btn" type="button" data-copy="${a(o.link)}">Copy</button><a class="btn" href="${a(o.url)}">Open</a></div></div>
|
|
418
|
-
<form class="group" data-action="${
|
|
447
|
+
<form class="group" data-action="${r}/visibility" data-busy="Saving\u2026" data-auto>
|
|
419
448
|
<span class="label">Visibility</span>
|
|
420
|
-
<div class="choices">${e.owner===null?`${
|
|
449
|
+
<div class="choices">${e.owner===null?`${p("private","Private","You, signed in on this computer with pocolobby settings.")}${p("public","Public","Anyone who can reach this address, without signing in.")}`:`${p("private","Private","You and the viewers below, after signing in with an email code.")}${p("public","Public","Anyone with the address, without signing in.")}`}</div>
|
|
421
450
|
<p class="warning">A public dashboard shows anyone who has the address your agents\u2019 names, what each does now and next, what they wait on from you, when they sent and received messages, how much of their Claude plans is left (without the accounts\u2019 addresses), and your notes\u2019 repositories and paths.</p>
|
|
422
|
-
${
|
|
451
|
+
${b}
|
|
423
452
|
</form>
|
|
424
453
|
</div>
|
|
425
454
|
<div class="card pad">
|
|
426
455
|
<div class="group"><span class="label">Icon</span><p class="hint">What browsers show for this dashboard in tabs and bookmarks, and on a phone\u2019s home screen.</p>
|
|
427
|
-
<div class="icon-now"><img src="${e.icon?`${a(o.settings)}/icon.png?v=${
|
|
428
|
-
<form class="row-actions" data-action="${
|
|
456
|
+
<div class="icon-now"><img src="${e.icon?`${a(o.settings)}/icon.png?v=${K(e.icon.sha256)}`:"/apple-touch-icon.png"}" alt="" width="56" height="56"><span>${e.icon?`This dashboard\u2019s own icon, ${Math.ceil(e.icon.png.length/1024)} KB.`:"PocoLobby\u2019s icon, until you upload your own."}</span>${e.icon?E(`${r}/icon/remove`,"Use PocoLobby\u2019s icon"):""}</div></div>
|
|
457
|
+
<form class="row-actions" data-action="${r}/icon" data-busy="Uploading\u2026"><label class="field grow">A square PNG, 32 to 1024 pixels wide, at most 64 KB<input name="png" type="file" accept="image/png" required></label><button class="btn end" type="submit">Upload icon</button>${b}</form>
|
|
429
458
|
</div>
|
|
430
459
|
${e.owner===null?"":`<div class="card pad">
|
|
431
|
-
<div class="group"><span class="label">Viewers</span><p class="hint">People who may open the private dashboard. Each signs in with a code sent to their address. You, ${a(e.owner)}, always can. Removing someone closes the dashboard to them at once.</p>${
|
|
432
|
-
<form class="row-actions" data-action="${
|
|
460
|
+
<div class="group"><span class="label">Viewers</span><p class="hint">People who may open the private dashboard. Each signs in with a code sent to their address. You, ${a(e.owner)}, always can. Removing someone closes the dashboard to them at once.</p>${l}</div>
|
|
461
|
+
<form class="row-actions" data-action="${r}/viewers" data-busy="Adding\u2026"><label class="field grow">Add a viewer<input name="email" type="email" required maxlength="254" placeholder="name@example.com"></label><button class="btn end" type="submit">Add viewer</button>${b}</form>
|
|
433
462
|
<p class="hint">${g(e.sessions,"dashboard session")} open now.</p>
|
|
434
|
-
</div>`}</div>`)}
|
|
435
|
-
<div class="stack"><div class="card
|
|
436
|
-
|
|
463
|
+
</div>`}</div>`)}u(ea,"renderBoardAccess");function aa(s,e){const o=s.lobby,r=u(d=>`${o.api}/${d}`,"api"),p=e.pacing.mode==="subscription",l=u((d,c,n)=>`<label class="choice"><input type="radio" name="mode" value="${d}"${e.pacing.mode===d?" checked":""}><strong>${c}</strong><span>${n}</span></label>`,"option"),m=u((d,c)=>`<label class="field narrow">${c}<input name="${d}" type="number" step="0.5" min="-100" max="100" required value="${e.pacing[d]}"></label>`,"field"),i=e.agents.filter(d=>d.enabled).sort((d,c)=>Number(c.manage_rooms)-Number(d.manage_rooms)||d.agent_id.localeCompare(c.agent_id)).map(d=>{const c=e.config.agents[d.agent_id],n=e.week(d.agent_id),h=n?Ce(n.used,n.resets_at,e.now):null,k=h?`<span class="line">${n.account.startsWith("agent:")?"Its own account":a(n.account)}</span>${Se({used:Math.round(n.used),line:Math.round(h.line),lead:Math.round(h.lead),state:Ae(h.lead)})}`:"",v=d.manage_rooms?'<span class="muted">Coordinator, never paced</span>':`<form data-action="${a(r("agent-kind"))}" data-busy="Saving\u2026" data-auto><input type="hidden" name="agent_id" value="${a(d.agent_id)}"><select name="kind" aria-label="Role of ${a(c?.name??q(d.agent_id))}"><option value="worker"${d.kind==="worker"?" selected":""}>Worker</option><option value="on_demand"${d.kind==="on_demand"?" selected":""}>On-demand</option></select>${b}</form>`;return`<li class="paced ${d.manage_rooms?"coordinator":d.kind==="worker"?"worker":"on-demand"}"><span class="main"><span class="avatar" aria-hidden="true">${a(c?.emoji??d.agent_id[0].toUpperCase())}</span><strong>${a(c?.name??q(d.agent_id))}</strong>${k}</span><span class="pick">${v}</span></li>`});return w(s,`Pacing in ${o.name}`,`${x("Pacing","Keeps room on each Claude subscription: a worker takes no new tasks while its account\u2019s week is ahead of its line, and always finishes what it has.")}
|
|
464
|
+
<div class="stack"><div class="card pad">
|
|
465
|
+
<form class="group" data-action="${a(r("pacing"))}" data-busy="Saving\u2026" data-auto>
|
|
466
|
+
<span class="label">Pacing</span>
|
|
467
|
+
<div class="choices">${l("off","No pacing","Every agent takes every task.")}${l("subscription","Subscription based","Each worker by its own Claude account\u2019s week, as its status line reports it.")}</div>
|
|
468
|
+
${b}
|
|
469
|
+
</form>
|
|
470
|
+
${p?`<form class="group" data-action="${a(r("pacing"))}" data-busy="Saving\u2026" data-auto data-stay>
|
|
471
|
+
<span class="label">Limits</span>
|
|
472
|
+
<p class="hint">A worker takes no new tasks once its account is more than the first many points ahead of its week\u2019s line, and new tasks again once it is the second far behind. Replies and notices about its work still reach it; new assignments and requests are refused, so the coordinator gives them to someone open.</p>
|
|
473
|
+
<div class="row-actions limits">${m("hold_at","No new tasks above")}${m("open_at","New tasks again at")}${b}</div>
|
|
474
|
+
</form>`:""}
|
|
475
|
+
</div>
|
|
476
|
+
${p?`<div class="card clip"><ul class="rows">${i.join("")}</ul></div>`:""}</div>`)}u(aa,"renderPacing");function sa(s,e){const o=s.lobby.id,r=e.agents.filter(i=>i.enabled),p=new Map(r.map(i=>[i.agent_id,e.curates(i.agent_id)])),l=u(i=>i.last_seen_at===null&&p.get(i.agent_id).length>0,"headless"),m=r.sort((i,d)=>Number(d.manage_rooms)-Number(i.manage_rooms)||Number(l(i))-Number(l(d))||+(e.config.agents[i.agent_id]?.support===!0)-+(e.config.agents[d.agent_id]?.support===!0)||i.agent_id.localeCompare(d.agent_id)).map(i=>{const d=e.config.agents[i.agent_id],c=e.rooms.filter(v=>v.members.includes(i.agent_id)).map(v=>v.room_id),n=p.get(i.agent_id),h=i.paused?`<span class="chip c-off">${i.resume_at?`Resting until ${a(S(i.resume_at))}`:"Resting"}</span>`:i.pacing?.state==="no_new_tasks"?'<span class="chip c-off">No new tasks</span>':e.connected(i.agent_id)?'<span class="chip c-ok">Bridge connected</span>':l(i)?'<span class="chip c-ok">Runs headless</span>':i.runtime_id===null?'<span class="chip c-warn">Setup pending</span>':'<span class="chip c-off">Bridge offline</span>',k=i.last_message_at?`<div class="files-line">last message ${a(S(i.last_message_at))}</div>`:"";return`<li><span class="main"><span class="avatar" aria-hidden="true">${a(d?.emoji??i.agent_id[0].toUpperCase())}</span><strong>${a(d?.name??q(i.agent_id))}</strong> <span class="muted">${i.manage_rooms?"coordinator":i.kind==="worker"?"worker":"on-demand"}, ${a(c.join(", ")||"no rooms")}${n.length?`, curates ${a(n.join(", "))}`:""}${d?.hidden?", not on the dashboard":d?.support?", support":""}</span>${k}</span>${h}</li>`});return w(s,`Agents in ${s.lobby.name}`,`${x("Agents","Admitting agents and their credentials stays with the server operator\u2019s CLI; their roles are under Pacing. A connected bridge does not mean the agent is free.",'<button class="btn" disabled>Admit agent</button>')}<div class="card">${m.length?`<ul class="rows">${m.join("")}</ul>`:T("mouse-envelope","No agents yet",`The server operator admits each agent with <span class="mono">pocolobby agent add NAME --lobby ${a(o)}</span>.`)}</div>`)}u(sa,"renderAgents");const C=u(s=>Array.isArray(s)&&s.length?s.map(String).join(", "):"","list");function Pe(s){const e=s.target??"",o=s.detail??{},r=`#${e}`;switch(s.action){case"create_lobby":return"created the lobby";case"create_room":return`created room ${r}`;case"set_room_members":return`set the members of ${r} to ${C(o.members)||"nobody"}`;case"change_room_members":{const p=[];return C(o.add)&&p.push(`added ${C(o.add)} to ${r}`),C(o.remove)&&p.push(`removed ${C(o.remove)} from ${r}`),p.join(" and ")||`changed the members of ${r}`}case"delete_room":return`deleted room ${r}`;case"archive_room":return`archived room ${r}`;case"restore_room":return`restored room ${r}`;case"board_source_set":return`set ${r} to read ${String(o.repo??"a repository")}${o.ref?` at ${String(o.ref)}`:""}${C(o.notes)?`: ${C(o.notes)}`:""}`;case"board_source_remove":return`stopped reading notes for ${r}`;case"board_agents":return"changed how agents look on the dashboard";case"summary_settings":return`set summaries to ${R[o.model]??String(o.model)}, ${String(o.effort)} reasoning`;case"board_visibility":return`made the dashboard ${e}`;case"board_icon":return"gave the dashboard its own icon";case"display_name":return e?`named the lobby \u201C${e}\u201D`:"went back to showing the lobby\u2019s name";case"currency":return`showed costs in ${e}`;case"pacing":{const p=o.lead===null||o.lead===void 0?"":`, at a lead of ${Number(o.lead)>0?"+":""}${Number(o.lead)}`;return o.state==="no_new_tasks"?`stopped new tasks for ${e}${p}`:`gave ${e} new tasks again${p}`}case"pacing_settings":return o.mode==="off"?"turned pacing off":`set pacing by subscription: no new tasks above ${String(o.hold_at)}, new tasks again at ${String(o.open_at)}`;case"agent_kind":return`made ${e} ${o.kind==="worker"?"a worker":"on-demand"}`;case"board_icon_remove":return"gave the dashboard PocoLobby\u2019s icon again";case"viewer_add":return`added ${e} as a dashboard viewer`;case"viewer_remove":return`removed ${e} as a dashboard viewer`;case"grant_lobby":return`made ${e} the lobby owner`;case"ungrant_lobby":return`ended ${e}\u2019s ownership`;case"repo_approve":return`approved ${e} for notes`;case"repo_remove":return`withdrew the approval of ${e}`;case"provision":return`admitted agent ${e}`;case"update_agent":return`changed agent ${e}${C(o)?` (${C(o)})`:""}`;case"rotate_token":return`rotated ${e}\u2019s credential`;case"reset_runtime":return`reset ${e}\u2019s runtime`;case"retry_notifications":return`retried notifications for ${e}`;case"rest_agent":return o.resume_at?`rested ${e} until ${S(String(o.resume_at))}`:`rested ${e}`;case"resume_agent":return o.timer?`resumed ${e} at its set time`:`resumed ${e}`;case"resume_runtime":return"resumed its session";case"tokens_clear":return`deleted the token totals before ${e}`;case"set_memory_curator":return o.curator?`made ${String(o.curator)} the curator of ${r}\u2019s memory`:`ended ${r}\u2019s shared memory curation`;case"erase_memory_entry":return`erased memory entry ${String(o.entry)} from ${r}`;case"erase_memory_fragment":return`erased a memory fragment from ${r}`;default:return`${s.action.replaceAll("_"," ")}${e?` ${e}`:""}`}}u(Pe,"describe");function ta(s,e){const o=s.lobby.id,r=e.events.map(p=>{const l=!!p.detail?.timer,m=p.action==="pacing",i=l||m?"PocoLobby":p.actor_kind==="operator"?"The operator":p.actor??"someone",d=l?"Timer":m?"Pace":p.actor_kind==="agent"?"API":p.actor_kind==="account"?"Settings":"CLI";return`<li><span class="main"><strong>${a(i)}</strong> ${a(Pe(p))} <span class="muted">${a(S(p.at))}</span></span><span class="chip c-off">${d}</span></li>`});return w(s,`Activity in ${s.lobby.name}`,`${x("Activity","Every change to the lobby, its rooms and its settings, and who made it: you in Settings, agents through the API, and the operator through the CLI.")}<div class="card">${r.length?`<ul class="rows">${r.join("")}</ul>`:T("luke-sleep","Nothing has happened yet","Changes to the lobby, its rooms and its settings show up here, with who made them.")}</div>`)}u(ta,"renderActivity");const Re=["January","February","March","April","May","June","July","August","September","October","November","December"],M=u(s=>s.toLocaleString("en-US"),"exact");function Ie(s,e){return s==="week"?`Week ${Number(e.slice(6))}, ${e.slice(0,4)}`:s==="month"?`${Re[Number(e.slice(5))-1]} ${e.slice(0,4)}`:A(`${e}T00:00:00Z`)}u(Ie,"periodLabel");function oa(s,e){const o=s.lobby,r=u(d=>z(d,o.money),"dollars"),p=[["day","Days"],["week","Weeks"],["month","Months"]].map(([d,c])=>`<a href="${a(o.settings)}/tokens?by=${d}"${d===e.by?' aria-current="page"':""}>${c}</a>`).join(""),l=e.periods.reduce((d,c)=>d+c.cost,0),m=e.periods.some(d=>d.unpriced),i=e.periods.map(d=>`<li><span class="period-head"><strong>${a(Ie(e.by,d.period))}</strong><b>${d.unpriced?"at least ":""}${r(d.cost)}</b></span>${d.models.map(c=>`<span class="model"><b>${a(c.model)}</b> ${c.cost===null?"no list price":r(c.cost)}: ${M(c.input)} in, ${M(c.cache_write)} cache write, ${M(c.cache_read)} cache read, ${M(c.output)} out, ${g(c.calls,"call")}</span>`).join("")}</li>`);return w(s,`Tokens in ${o.name}`,`${x("Tokens","What the agents' model calls would cost at Anthropic's API list prices, from their Claude Code transcripts: not what the Claude subscriptions cost. Days are UTC days.",`<div class="seg" role="group" aria-label="Group by">${p}</div>`)}
|
|
477
|
+
<div class="stack"><div class="card">${i.length?`<ul class="periods">${i.join("")}</ul>`:T("mouse-empty","No token counts yet","An agent's status line sends them each minute it runs.")}</div>
|
|
478
|
+
${i.length?`<p class="hint">${m?"At least ":""}${r(l)} in all, at the list prices of ${a(e.prices_as_of)}.${m?" A model without a list price counts in tokens only.":""}${a(B(o.money))}</p>`:""}
|
|
437
479
|
<form class="card pad group" data-action="${a(o.api)}/tokens/clear" data-busy="Deleting\u2026">
|
|
438
480
|
<span class="label">Delete old totals</span>
|
|
439
481
|
<p class="hint">Deletes the totals of the days before a date. Only days before ${a(A(`${e.clearable}T00:00:00Z`))} can go: the last four weeks can still take late reports. This cannot be undone.</p>
|
|
440
482
|
<div class="row-actions apart"><label class="field date">Delete the days before<input type="date" name="before" required max="${a(e.clearable)}"></label><button class="btn danger end" type="submit">Delete</button></div>
|
|
441
|
-
${
|
|
442
|
-
</form></div>`)}
|
|
443
|
-
<div class="stack">${
|
|
444
|
-
`):`<div class="card">${
|
|
445
|
-
<p class="hint">Costs on the dashboard and in Settings, kept in US dollars at Anthropic's list prices, shown in this currency. ${a(
|
|
446
|
-
<div class="row-actions"><label class="field">Show costs in<select name="currency">${
|
|
483
|
+
${b}
|
|
484
|
+
</form></div>`)}u(oa,"renderTokens");const ze={gotcha:"Gotchas",procedure:"Procedures",fix:"Fixes"},Z=8,Me=u(s=>s.length<2?s.join(""):`${s.slice(0,-1).join(", ")} and ${s.at(-1)}`,"names");function na(s,e){const o=s.lobby,r=u(c=>z(c,o.money),"dollars"),p=u(c=>`${o.api}/${c}`,"api"),l=u((c,n)=>E(p("memory/erase"),"Erase",{kind:c,key:n},"danger"),"erase"),m=u((c,n,h)=>`<details class="more"><summary>${a(c)}<span class="count">${a(n)}</span></summary>${h}</details>`,"more"),i=u(c=>`<div class="card"><ul class="memory">${c.join("")}</ul></div>`,"items"),d=e.rooms.map(c=>{const n=e.memory.find(t=>t.room_id===c.room_id),h=n?.curator??null,k=[["none","No shared memory"],...c.members.map(t=>[t,t])].map(([t,_])=>`<option value="${a(t)}"${(h??"none")===t?" selected":""}>${a(_)}</option>`).join(""),v=`<form class="row-actions" data-action="${a(p("memory/curator"))}" data-busy="Saving\u2026" data-auto><input type="hidden" name="room_id" value="${a(c.room_id)}"><label class="field">Curator<select name="agent_id">${k}</select></label>${b}</form>`;if(!n)return`<section class="card pad mem"><div class="mem-head"><h2>Room ${a(c.room_id)}</h2></div><p class="hint">No shared memory. Name a curator, a member who curates it with the code at hand, to start one.</p>${v}</section>`;const y=n.entries.filter(t=>t.state==="active"),L=n.entries.filter(t=>t.state==="archived"),j=n.fragments.filter(t=>t.state==="pending"),P=n.fragments.filter(t=>t.state==="refused"),D=[h?`<span class="chip c-ok">Curated by ${a(h)}</span>`:'<span class="chip c-off">No curator</span>',...n.failures?[`<span class="chip c-warn">${g(n.failures,"failed run")} in a row</span>`]:[]],N=n.curations[0],O=[g(y.length,"entry","entries"),`${j.length} waiting`,N?`last run ${S(N.at)}${N.error?", failed":`: ${N.added} added, ${N.updated} updated, ${N.refused} refused`}`:"no runs yet"],I=y.filter(t=>!t.checked_at).length,f=y.map(t=>t.checked_at).filter(t=>t!==null).sort()[0],X=I?`${g(I,"entry","entries")} not checked against the code yet.`:f?`Every entry checked against the code since ${S(f)}.`:"",Q=n.loaded<y.length?`Every session start loads ${n.loaded} of the ${y.length} rule lines; members find the rest with recall.`:"Every session start loads all its rule lines.",F=["gotcha","procedure","fix"].map(t=>[t,y.filter(_=>_.kind===t).length]).filter(([,t])=>t>0),U=y.filter(t=>t.disputed).length,ee=y.length>1?`<div class="mem-tools"><input type="search" data-memory-search placeholder="Search ${g(y.length,"entry","entries")}" aria-label="Search the memory of ${a(c.room_id)}">${F.length>1||U?`<div class="seg" role="group" aria-label="Kinds"><button type="button" data-kind-filter="" aria-pressed="true">All ${y.length}</button>${F.map(([t,_])=>`<button type="button" data-kind-filter="${t}" aria-pressed="false">${ze[t]} ${_}</button>`).join("")}${U?`<button type="button" data-kind-filter="disputed" aria-pressed="false">Disputed ${U}</button>`:""}</div>`:""}</div>`:"",ae=[...y].sort(ce).map(t=>{const _=[...new Set(t.provenance.map($=>$.author))],H=t.provenance.filter($=>$.evidence),ie=[`${a(t.ref)}, added ${a(A(t.created_at))}`,_.length?`Taught by ${a(Me(_))}${t.curator_only?", its curator alone":""}`:"Taught by nobody on record",t.checked_commit?`checked at ${a(t.checked_commit.slice(0,8))} on ${a(A(t.checked_at))}`:"not checked yet"];return`<li data-kind="${t.kind}"${t.disputed?" data-disputed":""} data-keywords="${a(t.keywords)}"><details><summary class="entry-line">${ye(t,$=>a(Ee($)),_.map($=>e.people[$]??{name:$,emoji:null}),t.disputed?'<span class="flag">Disputed</span>':"")}</summary><div class="mem-body"><p class="entry-full">${a(t.title)}</p><p>${a(t.text)}</p>${t.disputed?`<p class="disputed">Disputed: ${a(t.disputed)}</p>`:""}<p class="meta">${ie.join(" \xB7 ")}</p>${H.length?`<details class="sub"><summary>Evidence</summary><ul>${H.map($=>`<li><b>${a($.author)}:</b> ${a($.evidence)}</li>`).join("")}</ul></details>`:""}${t.versions.length?`<details class="sub"><summary>${g(t.changes,"earlier version")}</summary>${t.versions.map($=>`<p class="meta">${a(A($.changed_at))}: <b>${a($.title)}</b></p><p>${a($.text)}</p>`).join("")}</details>`:""}<div class="actions">${l("entry",t.ref)}</div></div></details></li>`}),se=y.length?`<span class="mem-count" data-count>All ${y.length}, newest first</span><ul class="mem-list" data-latest="${Z}">${ae.join("")}</ul>${y.length>Z?`<button class="btn" type="button" data-show-all hidden>Show all ${y.length}</button>`:""}`:'<p class="hint">No entries yet. Members teach the room with remember, and the curator turns what holds into entries.</p>',te=j.map(t=>`<li><span class="entry-head"><strong>${a(t.author)}, ${a(t.kind)}, ${a(A(t.created_at))}</strong>${l("fragment",t.id)}</span><p>${a(t.text)}</p>${t.evidence?`<span class="meta">Evidence: ${a(t.evidence)}</span>`:""}</li>`),oe=P.map(t=>`<li><span class="entry-head"><strong>${a(t.author)}, ${a(A(t.created_at))}</strong>${l("fragment",t.id)}</span><p>${t.text?a(t.text):'<span class="muted">Erased by the curator.</span>'}</p><span class="meta">Refused: ${a(t.note??"")}</span></li>`),ne=L.map(t=>`<li><span class="entry-head"><strong>${a(t.ref)}: ${a(t.title)}</strong>${l("entry",t.ref)}</span><span class="meta">${a(t.reason??"")}${t.into?` Merged into ${a(t.into)}.`:""} ${a(A(t.updated_at))}.</span></li>`),W=n.curations.map(t=>`<li><span class="entry-head"><strong>${a(S(t.at))}, ${a(t.curator)}</strong><b>${typeof t.usage?.cost_usd=="number"?r(t.usage.cost_usd):""}</b></span>${t.error?`<p class="disputed">Failed: ${a(t.error)}</p>`:`<span class="meta">${t.added} added, ${t.updated} updated, ${t.verified} verified, ${t.archived} archived, ${t.refused} refused${t.commit?` at ${a(t.commit.slice(0,8))}`:""}</span>${t.summary?`<p>${a(t.summary)}</p>`:""}`}</li>`),Y=n.curations.reduce((t,_)=>t+(typeof _.usage?.cost_usd=="number"?_.usage.cost_usd:0),0),re=[m(h?`Waiting for ${h}`:"Waiting for a curator",String(j.length),`${n.authors.length?`<p class="hint">Fragments this week: ${a(n.authors.map(t=>`${t.author} ${t.fragments}`).join(", "))}.</p>`:""}${j.length?i(te):'<p class="hint">Nothing waiting.</p>'}`),...P.length?[m("Refused in the last 30 days",String(P.length),i(oe))]:[],...L.length?[m("Archived",String(L.length),i(ne))]:[],m("Curation runs",`${n.curations.length}${Y?`, ${r(Y)}`:""}`,`<p class="hint">Costs are at API list prices${n.curations.length>=20?", over the latest 20 runs":""}.${a(B(o.money))}</p>${W.length?i(W):""}`),m("Curator",h??"none",v)];return`<section class="card pad mem"><div class="mem-head"><h2>Room ${a(c.room_id)}</h2>${D.join("")}</div><p class="hint">${a(O.join(" \xB7 "))}</p><p class="hint">${a([X,Q].filter(Boolean).join(" "))}</p>${ee}${se}<div class="mem-more">${re.join("")}</div></section>`});return w(s,`Memory in ${o.name}`,`${x("Memory","What each room's agents taught each other, curated against the code by the room's curator. Each agent's own memory is Claude Code's auto memory and is not shown here.")}
|
|
485
|
+
<div class="stack">${d.length?d.join(`
|
|
486
|
+
`):`<div class="card">${T("mouse-empty","No rooms yet","Shared memory belongs to a room and its members. Create a room first.")}</div>`}</div>`)}u(na,"renderMemory");function De(s){const e=new Intl.DisplayNames(["en"],{type:"currency"}),o=["USD",...Object.keys(s.table?.rates??{}).filter(m=>m!=="USD").sort()];o.includes(s.chosen)||o.push(s.chosen);const r=o.map(m=>`<option value="${a(m)}"${m===s.chosen?" selected":""}>${a(m)}, ${a(e.of(m)??m)}</option>`).join(""),p=s.chosen==="USD"||!!s.table?.rates?.[s.chosen],l=!s.fetching&&!s.table?.rates?"This server fetches no exchange rates (POCOLOBBY_EXCHANGE_RATES is not set), so costs show in US dollars.":p?s.table?.date?`The European Central Bank's reference rates of ${s.table.date}, fetched once a day; on weekends and holidays the last working day's.`:"No exchange rates fetched yet; costs show in US dollars until they are.":`No rate for ${s.chosen} is stored now, so costs show in US dollars until there is one.`;return`<span class="label">Currency</span>
|
|
487
|
+
<p class="hint">Costs on the dashboard and in Settings, kept in US dollars at Anthropic's list prices, shown in this currency. ${a(l)} The summaries' daily limit stays in US dollars.</p>
|
|
488
|
+
<div class="row-actions"><label class="field">Show costs in<select name="currency">${r}</select></label></div>`}u(De,"currencyChoice");function ra(s,e){const o=s.lobby;return w(s,`General settings for ${o.name}`,`${x("General","The lobby itself. Only its owner can change these.")}
|
|
447
489
|
<div class="card pad">
|
|
448
490
|
<div class="group"><span class="label">Name and address</span><div class="addr"><code>${a(o.id)}</code><span class="muted">${a(o.address)}, created ${a(A(e.created))}</span></div><p class="hint">A lobby\u2019s name cannot change: agents\u2019 credentials and the dashboard address depend on it.</p></div>
|
|
449
491
|
<form class="group" data-action="${a(o.api)}/display-name" data-busy="Saving\u2026" data-auto>
|
|
450
492
|
<span class="label">Display name</span>
|
|
451
493
|
<p class="hint">How the dashboard and Settings show the lobby, with capitals and spaces if you like, such as My Team for my-team. Leave it empty to show its name.</p>
|
|
452
494
|
<div class="row-actions"><label class="field grow">Display name<input name="display_name" maxlength="40" autocomplete="off" value="${a(e.display_name??"")}" placeholder="${a(o.id)}"></label></div>
|
|
453
|
-
${
|
|
495
|
+
${b}
|
|
454
496
|
</form>
|
|
455
497
|
<form class="group" data-action="${a(o.api)}/currency" data-busy="Saving\u2026" data-auto data-stay>
|
|
456
|
-
${
|
|
457
|
-
${
|
|
498
|
+
${De(e.currency)}
|
|
499
|
+
${b}
|
|
458
500
|
</form>
|
|
459
|
-
<div class="group"><span class="label">Theme on this browser</span><p class="hint">How the dashboard and Settings look here. System follows this device\u2019s light or dark mode.</p>${
|
|
501
|
+
<div class="group"><span class="label">Theme on this browser</span><p class="hint">How the dashboard and Settings look here. System follows this device\u2019s light or dark mode.</p>${we}</div>
|
|
460
502
|
<form class="group" data-action="${a(o.api)}/delete" data-busy="Deleting\u2026">
|
|
461
503
|
<span class="label">Delete lobby</span>
|
|
462
504
|
<p>Deleting removes every agent credential, room, message, attachment and summary in this lobby, and its dashboard. ${e.agents?`Its ${g(e.agents,"agent")} are disconnected at once. `:""}The name is never used again. This cannot be undone.</p>
|
|
463
505
|
<label class="field narrow">Type the lobby name to confirm<input name="confirm" required autocomplete="off" placeholder="${a(o.id)}"></label>
|
|
464
506
|
<div class="row-actions"><button class="btn danger" type="submit">Delete ${a(o.id)}</button></div>
|
|
465
|
-
${
|
|
507
|
+
${b}
|
|
466
508
|
</form>
|
|
467
|
-
</div>`)}
|
|
509
|
+
</div>`)}u(ra,"renderSettings");export{Pe as describe,ta as renderActivity,sa as renderAgents,ea as renderBoardAccess,Je as renderHome,na as renderMemory,Ke as renderNotice,Ze as renderOverview,aa as renderPacing,Xe as renderRooms,ra as renderSettings,Ve as renderSignIn,Qe as renderSummaries,oa as renderTokens};
|