ccdeck 3.44.0 → 3.45.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/README.md +14 -5
- package/THIRD_PARTY_NOTICES.md +27 -0
- package/dist/web/assets/BrowserWatchModal-Cbzf3TWa.js +1 -0
- package/dist/web/assets/IntegrationModal-BnnBr0a3.js +5 -0
- package/dist/web/assets/RadioPanel-BIqrMLQN.js +1 -0
- package/dist/web/assets/{TrafficRadar-Ch0LVdSl.js → TrafficRadar-D1gZCIwX.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-Du0ZkIJ1.js → UsageHistoryModal-DAhplVju.js} +1 -1
- package/dist/web/assets/app-BW3NYUYB.js +318 -0
- package/dist/web/assets/app-C5RY_wZ4.css +1 -0
- package/dist/web/assets/{index-BQbOfxbL.js → index-r8Ymr47Q.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/release-notes.json +18 -0
- package/src/server/account-notify.mjs +68 -6
- package/src/server/account-watch.mjs +61 -2
- package/src/server/claude-accounts.mjs +12 -2
- package/src/server/deck-prefs.mjs +15 -5
- package/src/server/feature-use.mjs +11 -1
- package/src/server/forge-http.mjs +514 -0
- package/src/server/forge-public-fetch.mjs +228 -0
- package/src/server/forge-routes.mjs +154 -0
- package/src/server/github-api.mjs +1369 -0
- package/src/server/gitlab-api.mjs +1271 -0
- package/src/server/index.mjs +6 -2
- package/src/server/integrations/cache.mjs +42 -0
- package/src/server/integrations/errors.mjs +167 -0
- package/src/server/integrations/forge.mjs +76 -0
- package/src/server/integrations/jira.mjs +1026 -0
- package/src/server/integrations/providers.mjs +11 -0
- package/src/server/integrations/proxy.mjs +314 -0
- package/src/server/integrations/registry.mjs +142 -0
- package/src/server/integrations/routes.mjs +498 -0
- package/src/server/integrations/store.mjs +226 -0
- package/src/server/lan-deck.mjs +7 -0
- package/src/server/lan-elsewhere.d.mts +30 -0
- package/src/server/lan-elsewhere.mjs +137 -0
- package/src/server/org-telemetry.mjs +173 -0
- package/src/server/pinned-build.mjs +3 -0
- package/src/server/request-gates.mjs +19 -0
- package/src/server/usage-day.mjs +2 -0
- package/dist/web/assets/BrowserWatchModal-DMIUygWz.js +0 -1
- package/dist/web/assets/RadioPanel-DNB_AHSs.js +0 -1
- package/dist/web/assets/app-CQIstkMF.js +0 -318
- package/dist/web/assets/app-Ch-0CYlK.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./app-
|
|
1
|
+
import"./app-BW3NYUYB.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();
|
package/dist/web/index.html
CHANGED
|
@@ -77,9 +77,9 @@
|
|
|
77
77
|
document.documentElement.setAttribute("data-density", density === "tightest" || density === "tighter" || density === "compact" || density === "comfortable" ? density : "default");
|
|
78
78
|
})();
|
|
79
79
|
</script>
|
|
80
|
-
<script type="module" crossorigin src="/assets/index-
|
|
81
|
-
<link rel="modulepreload" crossorigin href="/assets/app-
|
|
82
|
-
<link rel="stylesheet" crossorigin href="/assets/app-
|
|
80
|
+
<script type="module" crossorigin src="/assets/index-r8Ymr47Q.js"></script>
|
|
81
|
+
<link rel="modulepreload" crossorigin href="/assets/app-BW3NYUYB.js">
|
|
82
|
+
<link rel="stylesheet" crossorigin href="/assets/app-C5RY_wZ4.css">
|
|
83
83
|
</head>
|
|
84
84
|
<body>
|
|
85
85
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccdeck",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.45.0",
|
|
4
4
|
"description": "Local dashboard for Claude Code and Codex CLI sessions: which Claude Code session needs input, every tool call and Claude Code subagent live, token cost and quota, several Claude accounts. Run npx ccdeck.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/release-notes.json
CHANGED
|
@@ -80,6 +80,24 @@
|
|
|
80
80
|
"3.43.1",
|
|
81
81
|
"3.43.3"
|
|
82
82
|
],
|
|
83
|
+
"3.45.0": [
|
|
84
|
+
{
|
|
85
|
+
"title": "🔗 Read-only Jira, GitHub and GitLab panels beside your agents",
|
|
86
|
+
"body": "Settings › Integrations connects Jira (Cloud, Server or Data Center), GitHub, and GitLab.com or your own GitLab, each with a token, and a control on the left stripe opens its panel: a Jira board's backlog in Jira's own order, or a project's branches, pull or merge requests, pipelines and issues, with the item beside the list. The panels only read: they never comment, merge, re-run a job or change an issue. The token stays on this machine, in a file only you can read, and goes only to that connection's own API. Nothing is read until you connect a tracker, and then only while its panel is open."
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"title": "📤 Each account says what its organization's telemetry sends",
|
|
90
|
+
"body": "A Team or Enterprise organization can turn on Claude Code's telemetry for every member, and Claude Code applies it without asking. Each account of such an organization now carries a line under its name: “Telemetry · usage data”, or “Telemetry · prompts, tool details” in the warning colour when your prompts and tool details are sent too. Its tooltip names the organization and where the data goes. After a switch, Auto-switch's notification and the row's “Now active” note say it too. The deck changes nothing; it learns what the organization turns on from a Claude Code session run on one of its accounts, and until then that account shows no line."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"title": "⚠️ A warning when a paired deck is on the same Claude login",
|
|
94
|
+
"body": "A Claude login renews once, so when two machines use one copied login, the one that renews second is signed out. When a paired deck on Local network says it is on the login this deck is using, the live account's row now says “Also active on” and names that deck, with what to do: use a different account on one of the machines, or sign in on each one separately. A switch onto such a login says so too, and so does one desktop notification per incident, “Login active on another deck” in Settings › Notifications, which is on by default."
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"title": "📱 A phone opens one panel at most when the deck loads",
|
|
98
|
+
"body": "On a window 640 px wide or less, a reload used to open Accounts, Usage and the machine panel on top of each other, with none of the board in sight. It now opens at most one of them, preferring Usage over the machine panel, and Accounts and the session list open from the dock. A wider window loads exactly as before, and nothing you chose is forgotten."
|
|
99
|
+
}
|
|
100
|
+
],
|
|
83
101
|
"3.44.0": [
|
|
84
102
|
{
|
|
85
103
|
"title": "⛔ A turn that stops on an error now shows as stopped",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Five notifications about the accounts the deck is on, and the rules for when
|
|
2
2
|
// each is said.
|
|
3
3
|
//
|
|
4
4
|
// SWAP the deck's own auto-switch tick moved the live Claude account.
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// every session on the machine shares the login, so they stop
|
|
14
14
|
// together — with how many stopped and that each needs a
|
|
15
15
|
// "continue" once a working login is active.
|
|
16
|
+
// ELSEWHERE the Claude account this deck is on is the one a paired deck says
|
|
17
|
+
// it is on too (#2192). Said once per incident — from the moment
|
|
18
|
+
// both are on it until one of them is not — because the next
|
|
19
|
+
// renewal signs one of the two machines out, and nothing on the
|
|
20
|
+
// desktop says so before it does. See lan-elsewhere.mjs.
|
|
16
21
|
//
|
|
17
22
|
// SPARSE BY CONSTRUCTION. Each window is a record keyed by provider, account
|
|
18
23
|
// and window, holding the reset instant it is about and the levels already said
|
|
@@ -42,6 +47,7 @@
|
|
|
42
47
|
// injected — the shape block-notify.mjs uses, for the reason it gives: what the
|
|
43
48
|
// suite cannot run is what drifts.
|
|
44
49
|
import { switchCanCure } from "./stop-failure.mjs";
|
|
50
|
+
import { deckList } from "./lan-elsewhere.mjs";
|
|
45
51
|
|
|
46
52
|
/** The levels a window is announced at, lowest first. */
|
|
47
53
|
export const LEVELS = Object.freeze([90, 100]);
|
|
@@ -403,6 +409,23 @@ export function stuckNotice({ fromName }) {
|
|
|
403
409
|
return { who: SWAP_WHO, body: `${from}'s login expired · no other account is ready to switch to` };
|
|
404
410
|
}
|
|
405
411
|
|
|
412
|
+
/**
|
|
413
|
+
* "Claude · work@x.com", "Also active on iMac. When one machine renews this
|
|
414
|
+
* login, the other is signed out — use a different account on one of them."
|
|
415
|
+
*
|
|
416
|
+
* The decks by the names Local network's list gives them. The reason is the
|
|
417
|
+
* sentence, because it is the whole of why this is worth a notification: the
|
|
418
|
+
* same account on two machines is ordinary, one copied login live on two is
|
|
419
|
+
* what breaks, and the remedy is one press on either machine.
|
|
420
|
+
*/
|
|
421
|
+
export function elsewhereNotice({ accountName, decks }) {
|
|
422
|
+
return {
|
|
423
|
+
who: whoFor("claude", accountName),
|
|
424
|
+
body: `Also active on ${cut(deckList(decks), NAME_MAX)}. When one machine renews this login, the other is `
|
|
425
|
+
+ "signed out — use a different account on one of them.",
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
406
429
|
/** The records, as read back from disk: anything that is not one is dropped,
|
|
407
430
|
* and so is one nobody has read for RECORD_KEEP_MS. */
|
|
408
431
|
export function normaliseRecords(raw, now) {
|
|
@@ -435,9 +458,9 @@ function laterUnref(fn, ms) {
|
|
|
435
458
|
*
|
|
436
459
|
* notify(title, body, meta) the OS call — injected, never awaited by a poll
|
|
437
460
|
* or a tick, and its failure goes to `onError`
|
|
438
|
-
* settings() `{ swap, quota, reset, stopped }`,
|
|
439
|
-
* decision, so a switch pressed mid-run
|
|
440
|
-
* effect at once
|
|
461
|
+
* settings() `{ swap, quota, reset, stopped, elsewhere }`,
|
|
462
|
+
* asked per decision, so a switch pressed mid-run
|
|
463
|
+
* takes effect at once
|
|
441
464
|
* load() / save(records) the disk, both best effort
|
|
442
465
|
* now, later the clock and the hold timer, for the suite
|
|
443
466
|
*
|
|
@@ -459,6 +482,11 @@ export function createAccountNotifier({
|
|
|
459
482
|
let policy = null;
|
|
460
483
|
/** Threshold notices held for a tick, by record key. */
|
|
461
484
|
const held = new Map();
|
|
485
|
+
/** The account whose ELSEWHERE incident is open — this deck and another on
|
|
486
|
+
* it — and therefore already said, or null. One at a time, because this
|
|
487
|
+
* deck is on one account at a time. Kept in memory: a restart in the middle
|
|
488
|
+
* of an incident says it once more, which is a restart's news as well. */
|
|
489
|
+
let elsewhereOpen = null;
|
|
462
490
|
let holdTimer = null;
|
|
463
491
|
let saving = Promise.resolve();
|
|
464
492
|
/** The stop incident being gathered or held, or the one already said while
|
|
@@ -617,8 +645,13 @@ export function createAccountNotifier({
|
|
|
617
645
|
* `{ email, organizationUuid, alias }` — read after the tick, for the names
|
|
618
646
|
* and the keys of the two accounts a switch moved between. `startedAt` is
|
|
619
647
|
* when the tick began, which decides whether it can answer for a stop.
|
|
648
|
+
*
|
|
649
|
+
* `telemetryOf`, when given, answers what an organization's telemetry
|
|
650
|
+
* settings turn on, as one sentence or null (org-telemetry.mjs's
|
|
651
|
+
* telemetryClause): the swap notice carries it for the account it moved to,
|
|
652
|
+
* because an unattended switch is exactly when nobody saw it coming.
|
|
620
653
|
*/
|
|
621
|
-
async function tick(result, accounts, { startedAt = null } = {}) {
|
|
654
|
+
async function tick(result, accounts, { startedAt = null, telemetryOf = null } = {}) {
|
|
622
655
|
try {
|
|
623
656
|
await ready;
|
|
624
657
|
if (!result || typeof result !== "object") return;
|
|
@@ -686,7 +719,12 @@ export function createAccountNotifier({
|
|
|
686
719
|
cured.said = true;
|
|
687
720
|
if (cured.timer) { clearTimeout(cured.timer); cured.timer = null; }
|
|
688
721
|
}
|
|
689
|
-
|
|
722
|
+
const told = cured && s.stopped ? withStopped(notice, cured.ids.size) : notice;
|
|
723
|
+
// Only for an account the store and the tick agree on: an organization
|
|
724
|
+
// read off the wrong slot would put another company's policy on it.
|
|
725
|
+
const toOrg = to?.key ? accounts?.[String(result.to?.number)]?.organizationUuid : null;
|
|
726
|
+
const telemetry = toOrg && telemetryOf ? telemetryOf(toOrg) : null;
|
|
727
|
+
say(telemetry ? { ...told, body: `${told.body}\n${telemetry}` } : told);
|
|
690
728
|
}
|
|
691
729
|
await persist();
|
|
692
730
|
} catch (err) {
|
|
@@ -721,11 +759,35 @@ export function createAccountNotifier({
|
|
|
721
759
|
}
|
|
722
760
|
}
|
|
723
761
|
|
|
762
|
+
/**
|
|
763
|
+
* What Local network says about the account this deck is on: `account` is
|
|
764
|
+
* `{ key, name }`, or null when this deck is on none, and `decks` the paired
|
|
765
|
+
* decks that say they are on it too, by name.
|
|
766
|
+
*
|
|
767
|
+
* ONCE PER INCIDENT. The incident opens when both are on it, is said then —
|
|
768
|
+
* if the switch is on at that moment — and closes when either moves off it
|
|
769
|
+
* or the other deck stops saying so (lan-elsewhere.mjs holds a deck's word
|
|
770
|
+
* for five minutes, which is what keeps a dropped round from closing it). A
|
|
771
|
+
* new one is news again; the same one, every minute, is not.
|
|
772
|
+
*/
|
|
773
|
+
async function elsewhere(account, decks) {
|
|
774
|
+
try {
|
|
775
|
+
await ready;
|
|
776
|
+
const key = account?.key && Array.isArray(decks) && decks.length > 0 ? account.key : null;
|
|
777
|
+
if (key === elsewhereOpen) return;
|
|
778
|
+
elsewhereOpen = key;
|
|
779
|
+
if (key && settings().elsewhere) say(elsewhereNotice({ accountName: account.name, decks }));
|
|
780
|
+
} catch (err) {
|
|
781
|
+
onError?.(err);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
724
785
|
return {
|
|
725
786
|
ready,
|
|
726
787
|
observe,
|
|
727
788
|
tick,
|
|
728
789
|
stopped,
|
|
790
|
+
elsewhere,
|
|
729
791
|
/** The loop was switched on or off. Off, nothing will carry what is held. */
|
|
730
792
|
autoSwitch(on) {
|
|
731
793
|
autoOn = on === true;
|
|
@@ -24,6 +24,10 @@ import { writeFileAtomic } from "./atomic-write.mjs";
|
|
|
24
24
|
// What a StopFailure says, and the words a notification names a session by.
|
|
25
25
|
import { stopFailureOf, switchCanCure } from "./stop-failure.mjs";
|
|
26
26
|
import { whoOf } from "./block-notify.mjs";
|
|
27
|
+
// Which deck is on what, as Local network last heard it, and the identity rule
|
|
28
|
+
// its keys are made by (#2192).
|
|
29
|
+
import { elsewhereNow } from "./lan-elsewhere.mjs";
|
|
30
|
+
import { accountKey } from "./lan-copies.mjs";
|
|
27
31
|
|
|
28
32
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
29
33
|
|
|
@@ -42,10 +46,19 @@ export function recordsPath(home = deckDataDir()) {
|
|
|
42
46
|
*/
|
|
43
47
|
export const WATCH_MS = 2 * 60_000;
|
|
44
48
|
|
|
49
|
+
/**
|
|
50
|
+
* How often the notifier asks Local network whether the account this deck is
|
|
51
|
+
* on is live on another deck too (#2192). A round is a minute, so asking more
|
|
52
|
+
* often finds nothing new; and with no deck on any account of this one's, the
|
|
53
|
+
* look is the engine's status and nothing else — the store is not read.
|
|
54
|
+
*/
|
|
55
|
+
export const ELSEWHERE_WATCH_MS = 60_000;
|
|
56
|
+
|
|
45
57
|
let _deps = null;
|
|
46
58
|
let _notifier = null;
|
|
47
59
|
let _autoOn = false;
|
|
48
60
|
let _timer = null;
|
|
61
|
+
let _elsewhereTimer = null;
|
|
49
62
|
let _watching = null;
|
|
50
63
|
/** The auto-switch loop's "tick now", handed in by the loop as it loads
|
|
51
64
|
* (cswap-auto-loop.mjs `tickSoon`). Null until then, and on a deck that never
|
|
@@ -110,11 +123,23 @@ export function noteAutoTick(result, meta = {}) {
|
|
|
110
123
|
if (!n || !result) return Promise.resolve();
|
|
111
124
|
// A dead login with nowhere to go is named too, so its notice can say whose.
|
|
112
125
|
const named = result.switched || result.reason === "login-expired";
|
|
113
|
-
return (named ? storeAccounts() : Promise.resolve(null))
|
|
114
|
-
.then(accounts => n.tick(result, accounts, meta))
|
|
126
|
+
return (named ? Promise.all([storeAccounts(), result.switched ? telemetryReader() : null]) : Promise.resolve([null, null]))
|
|
127
|
+
.then(([accounts, telemetryOf]) => n.tick(result, accounts, { ...meta, telemetryOf }))
|
|
115
128
|
.catch(() => {});
|
|
116
129
|
}
|
|
117
130
|
|
|
131
|
+
/** What each organization's settings were last seen to turn on, as the
|
|
132
|
+
* sentence a swap notice carries (org-telemetry.mjs), or null. */
|
|
133
|
+
async function telemetryReader() {
|
|
134
|
+
try {
|
|
135
|
+
const { knownOrgTelemetry, telemetryClause } = await import(pathToFileURL(join(PKG_ROOT, "src/server/org-telemetry.mjs")).href);
|
|
136
|
+
const records = await knownOrgTelemetry();
|
|
137
|
+
return org => telemetryClause(records?.[org]);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
118
143
|
/** The loop's way to be asked for a tick now. Called once, as it loads. */
|
|
119
144
|
export function connectEarlyTick(fn) {
|
|
120
145
|
_tickSoon = typeof fn === "function" ? fn : null;
|
|
@@ -199,6 +224,35 @@ export function watchOnce(providers) {
|
|
|
199
224
|
return _watching;
|
|
200
225
|
}
|
|
201
226
|
|
|
227
|
+
/**
|
|
228
|
+
* One look at whether the account this deck is on is one a paired deck says it
|
|
229
|
+
* is on too, handed to the notifier, which says it once per incident.
|
|
230
|
+
*
|
|
231
|
+
* The store is read only when some deck is on some account, and through the
|
|
232
|
+
* roster's own cache, which the panel and every LAN round already keep warm. A
|
|
233
|
+
* roster that could not be read says nothing and closes nothing: not knowing
|
|
234
|
+
* which account this deck is on is not the same as being on none.
|
|
235
|
+
*/
|
|
236
|
+
export async function watchElsewhereOnce(now = Date.now()) {
|
|
237
|
+
const n = notifier();
|
|
238
|
+
if (!n) return;
|
|
239
|
+
try {
|
|
240
|
+
const on = elsewhereNow(now);
|
|
241
|
+
if (on.size === 0) return n.elsewhere(null, []);
|
|
242
|
+
const { fetchClaudeAccounts } = await import(pathToFileURL(join(PKG_ROOT, "src/server/claude-accounts.mjs")).href);
|
|
243
|
+
const got = await fetchClaudeAccounts();
|
|
244
|
+
if (!got?.ok || !Array.isArray(got.accounts)) return;
|
|
245
|
+
const live = got.accounts.find(a => a?.active === true);
|
|
246
|
+
if (!live?.email) return n.elsewhere(null, []);
|
|
247
|
+
const key = accountKey(live.email, live.orgUuid);
|
|
248
|
+
const name = typeof live.alias === "string" && live.alias.trim() ? live.alias.trim() : live.email;
|
|
249
|
+
return n.elsewhere({ key, name }, on.get(key) ?? []);
|
|
250
|
+
} catch {
|
|
251
|
+
// The roster's own failures are its panel's to say; here there is nothing
|
|
252
|
+
// to compare this time.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
202
256
|
/**
|
|
203
257
|
* Start reading quota on a timer, from startServer once the port is bound —
|
|
204
258
|
* never at import, because a launcher imports the server module only to ask
|
|
@@ -210,4 +264,9 @@ export function startAccountWatch({ providers } = {}) {
|
|
|
210
264
|
_timer.unref?.();
|
|
211
265
|
const first = setTimeout(() => { watchOnce(providers).catch(() => {}); }, 15_000);
|
|
212
266
|
first.unref?.();
|
|
267
|
+
// And Local network's word on the account this deck is on, on its own clock:
|
|
268
|
+
// it costs nothing while no deck is on anything, and it is said whichever of
|
|
269
|
+
// the quota switches is on.
|
|
270
|
+
_elsewhereTimer = setInterval(() => { watchElsewhereOnce().catch(() => {}); }, ELSEWHERE_WATCH_MS);
|
|
271
|
+
_elsewhereTimer.unref?.();
|
|
213
272
|
}
|
|
@@ -52,6 +52,10 @@ import { nextReadAt, nudgeCollector } from "./claude-collector.mjs";
|
|
|
52
52
|
// (#1893). Pure, and keyed the way LAN sync keys accounts.
|
|
53
53
|
import { hasRecovered, reauthFor } from "./account-origins.mjs";
|
|
54
54
|
import { accountKey } from "./lan-copies.mjs";
|
|
55
|
+
// What each organization's Claude settings were last seen to turn on, so a row
|
|
56
|
+
// can say it before anyone switches to the account. See its header for when the
|
|
57
|
+
// shared remote-settings.json can be pinned on an organization at all.
|
|
58
|
+
import { observeOrgTelemetry } from "./org-telemetry.mjs";
|
|
55
59
|
// Whether a switch target's stored login is known dead, and whether a
|
|
56
60
|
// collection would read it first (#2184). Pure, and imports nothing.
|
|
57
61
|
import { collectionWouldRead, deadLoginOf } from "./login-guard.mjs";
|
|
@@ -467,6 +471,7 @@ async function readRoster(now, gen) {
|
|
|
467
471
|
}
|
|
468
472
|
|
|
469
473
|
const rows = await readUsageRows(root);
|
|
474
|
+
const orgTelemetry = await observeOrgTelemetry({ seq }).catch(() => ({}));
|
|
470
475
|
|
|
471
476
|
// Kick a collection for the NEXT poll if anything is due — either because
|
|
472
477
|
// claude-swap's schedule says so, or because an account has never been
|
|
@@ -500,7 +505,7 @@ async function readRoster(now, gen) {
|
|
|
500
505
|
const row = rows[num];
|
|
501
506
|
const key = accountKey(acct.email, acct.organizationUuid);
|
|
502
507
|
const origin = Object.hasOwn(origins, key) ? origins[key] : null;
|
|
503
|
-
const shown = rosterRow({ seq, num, acct, row, identity, now, key, origin });
|
|
508
|
+
const shown = rosterRow({ seq, num, acct, row, identity, now, key, origin, telemetry: orgTelemetry[acct.organizationUuid] ?? null });
|
|
504
509
|
accounts.push(shown);
|
|
505
510
|
// A put-off incident is over once a read succeeds after it, and the
|
|
506
511
|
// put-off goes with it (#1893) — said here, where the read is, rather than
|
|
@@ -527,7 +532,7 @@ function observedMs(seconds) {
|
|
|
527
532
|
return Number.isFinite(ms) && ms > 0 && Number.isFinite(new Date(ms).getTime()) ? ms : null;
|
|
528
533
|
}
|
|
529
534
|
|
|
530
|
-
function rosterRow({ seq, num, acct, row, identity, now, key, origin = null }) {
|
|
535
|
+
function rosterRow({ seq, num, acct, row, identity, now, key, origin = null, telemetry = null }) {
|
|
531
536
|
const matches = rowIsFor(row, acct);
|
|
532
537
|
const good = matches ? row.lastGood : null;
|
|
533
538
|
|
|
@@ -617,6 +622,11 @@ function rosterRow({ seq, num, acct, row, identity, now, key, origin = null }) {
|
|
|
617
622
|
// claude-swap has refused its login since: `{ key, since, dismissed }`.
|
|
618
623
|
// `key` and `since` together name it, for "Not now" to send back.
|
|
619
624
|
reauth: reauth ? { key, ...reauth } : null,
|
|
625
|
+
// What this account's organization last turned on for Claude Code's
|
|
626
|
+
// telemetry export, when the deck has seen its settings (org-telemetry.mjs):
|
|
627
|
+
// `{ orgName, level, content, destinations, seenAt }`. `level` "off" was
|
|
628
|
+
// seen off; null was never seen, which is unknown and not "off".
|
|
629
|
+
telemetry: telemetry ?? null,
|
|
620
630
|
};
|
|
621
631
|
}
|
|
622
632
|
|
|
@@ -88,6 +88,10 @@ export const DEFAULTS = Object.freeze({
|
|
|
88
88
|
// incident (account-notify.mjs). ON for the swap's reason: it is rare, every
|
|
89
89
|
// running session is stopped when it happens, and nothing else says so.
|
|
90
90
|
notifyStopped: true,
|
|
91
|
+
// A login this deck is on, live on another deck too (#2192). ON, like the
|
|
92
|
+
// swap: it is rare, it is said once per incident, and nothing else on the
|
|
93
|
+
// desktop says it before one of the two machines is signed out.
|
|
94
|
+
notifyElsewhere: true,
|
|
91
95
|
// Continue a BACKGROUND session whose turn stopped on an expired login or a
|
|
92
96
|
// usage limit, once a working login with room is active again — once per
|
|
93
97
|
// stop (stop-continue.mjs, #2193). OFF: it stops and restarts a session's
|
|
@@ -344,6 +348,7 @@ export function normalise(raw) {
|
|
|
344
348
|
notifyQuota: flagOr(src.notifyQuota, DEFAULTS.notifyQuota),
|
|
345
349
|
notifyReset: flagOr(src.notifyReset, DEFAULTS.notifyReset),
|
|
346
350
|
notifyStopped: flagOr(src.notifyStopped, DEFAULTS.notifyStopped),
|
|
351
|
+
notifyElsewhere: flagOr(src.notifyElsewhere, DEFAULTS.notifyElsewhere),
|
|
347
352
|
continueStopped: flagOr(src.continueStopped, DEFAULTS.continueStopped),
|
|
348
353
|
tourSeen: flagOr(src.tourSeen, DEFAULTS.tourSeen),
|
|
349
354
|
autoUpdate: flagOr(src.autoUpdate, DEFAULTS.autoUpdate),
|
|
@@ -729,7 +734,10 @@ export function publicPrefs(prefs) {
|
|
|
729
734
|
* here — prefs-route-fields.test.ts lists them.
|
|
730
735
|
*/
|
|
731
736
|
export const PAGE_FIELDS = Object.freeze({
|
|
732
|
-
top: Object.freeze([
|
|
737
|
+
top: Object.freeze([
|
|
738
|
+
"notifications", "notifySwap", "notifyQuota", "notifyReset", "notifyStopped", "notifyElsewhere", "continueStopped",
|
|
739
|
+
"tourSeen", "autoUpdate", "recoverCodexSessions", "recoverClaudeSessions",
|
|
740
|
+
]),
|
|
733
741
|
lan: Object.freeze([
|
|
734
742
|
"enabled", "name", "shared", "manual", "shareActive", "sendInUse", "pairingMode",
|
|
735
743
|
"autoAsk", "autoAccept", "tailscale", "tailscaleAsk", "tailscaleAccept",
|
|
@@ -763,15 +771,17 @@ export function notificationsOn(prefs, env = process.env) {
|
|
|
763
771
|
}
|
|
764
772
|
|
|
765
773
|
/**
|
|
766
|
-
* Which account notifications may go out right now: the
|
|
767
|
-
* on its own, and all
|
|
774
|
+
* Which account notifications may go out right now: the five switches, each
|
|
775
|
+
* on its own, and all five off when the machine vetoed notifications at
|
|
768
776
|
* launch — AGENTS_DECK_NO_NOTIFY=1 keeps the deck off the desktop whatever
|
|
769
777
|
* else it was told, and these are desktop notifications too.
|
|
770
778
|
*/
|
|
771
779
|
export function accountNotifySettings(prefs, env = process.env) {
|
|
772
|
-
if (notificationsVetoed(env)) return { swap: false, quota: false, reset: false, stopped: false };
|
|
780
|
+
if (notificationsVetoed(env)) return { swap: false, quota: false, reset: false, stopped: false, elsewhere: false };
|
|
773
781
|
const p = normalise(prefs);
|
|
774
|
-
return {
|
|
782
|
+
return {
|
|
783
|
+
swap: p.notifySwap, quota: p.notifyQuota, reset: p.notifyReset, stopped: p.notifyStopped, elsewhere: p.notifyElsewhere,
|
|
784
|
+
};
|
|
775
785
|
}
|
|
776
786
|
|
|
777
787
|
/**
|
|
@@ -23,10 +23,20 @@ const ROUTE_FEATURES = new Map([
|
|
|
23
23
|
["POST /api/clear", "clear-board"],
|
|
24
24
|
]);
|
|
25
25
|
|
|
26
|
+
/** Features a whole namespace of routes is: a connected tracker's reads carry
|
|
27
|
+
* the connection's id in the path (#2178), so they are matched by prefix. The
|
|
28
|
+
* name is the provider's, never the connection, board or issue read. */
|
|
29
|
+
const ROUTE_PREFIX_FEATURES = [
|
|
30
|
+
["GET /api/integrations/jira/", "jira"],
|
|
31
|
+
["GET /api/integrations/github/", "github"],
|
|
32
|
+
["GET /api/integrations/gitlab/", "gitlab"],
|
|
33
|
+
];
|
|
34
|
+
|
|
26
35
|
/** Note the feature a request is, if it is one. Called for requests that got
|
|
27
36
|
* past the deck's own gates. */
|
|
28
37
|
export function noteRouteFeature(method, pathname) {
|
|
29
|
-
const
|
|
38
|
+
const route = `${method} ${pathname}`;
|
|
39
|
+
const name = ROUTE_FEATURES.get(route) ?? ROUTE_PREFIX_FEATURES.find(([prefix]) => route.startsWith(prefix))?.[1];
|
|
30
40
|
if (name) usageDay.noteFeature(name);
|
|
31
41
|
}
|
|
32
42
|
|