ccdeck 3.38.0 → 3.38.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.
@@ -64,8 +64,8 @@
64
64
  stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
65
65
  })();
66
66
  </script>
67
- <script type="module" crossorigin src="/assets/index-C_h8mR-c.js"></script>
68
- <link rel="stylesheet" crossorigin href="/assets/index-0cUs2Jed.css">
67
+ <script type="module" crossorigin src="/assets/index-DnnkOgia.js"></script>
68
+ <link rel="stylesheet" crossorigin href="/assets/index-D-HX6Mea.css">
69
69
  </head>
70
70
  <body>
71
71
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccdeck",
3
- "version": "3.38.0",
3
+ "version": "3.38.1",
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": {
@@ -70,7 +70,8 @@
70
70
  "3.36.8",
71
71
  "3.36.9",
72
72
  "3.37.0",
73
- "3.38.0"
73
+ "3.38.0",
74
+ "3.38.1"
74
75
  ],
75
76
  "3.37.1": [
76
77
  {
@@ -133,8 +133,11 @@ export const DEFAULTS = Object.freeze({
133
133
  // WHAT MAKES IT SURVIVABLE IS THE GATE THAT NEVER CHANGED: `shared` is
134
134
  // empty, so a deck that pairs is offered NOTHING until somebody ticks a
135
135
  // login here. Pairing is a name in a list; a login is the thing worth
136
- // having, and it still takes a deliberate tick on this machine. A deck on a
137
- // network it does not own turns this off in the dialog, in one press.
136
+ // having, and it still takes a deliberate tick on this machine. The other
137
+ // direction is not gated by a tick here, since 3.38.1: a deck the switch
138
+ // pairs adds and repairs the logins a person ticked on it, as any paired
139
+ // deck does (see roundWith), and never replaces one that works. So a deck
140
+ // on a network it does not own turns this off in the dialog, in one press.
138
141
  autoAsk: true,
139
142
  autoAccept: true,
140
143
  pairingMode: "automatic",
@@ -397,7 +397,8 @@ export function createEngine({
397
397
  * is new.
398
398
  *
399
399
  * Or one the accept switch chose for them, which `auto` says and the pin
400
- * keeps — see addTrusted, and roundWith for what such a deck may not do.
400
+ * keeps — see addTrusted, and sharedWith in lan-sync.mjs for what such a
401
+ * deck is not handed.
401
402
  */
402
403
  const pin = ({ fp, pub, name }, { auto = false } = {}) => {
403
404
  const { list, added } = addTrusted(cfg.trusted, { fp, pub, name, at: now(), ...(auto ? { auto: true } : {}) });
@@ -727,11 +728,11 @@ export function createEngine({
727
728
  if (!stillPaired()) throw new Error("peer no longer paired");
728
729
  if (theirs?.t !== "manifest" || !Array.isArray(theirs.accounts)) throw new Error("no manifest");
729
730
  const list = keepManifest(conn.key, theirs, conn.peerFp);
730
- // FROM A DECK SOMEBODY HERE CHOSE, NEITHER AN ADD NOR A HEAL NEEDS MY
731
- // TICK. Adding is the case the owner asked for by name: an account that
732
- // appears among the decks I paired with appears on all of them, which is
733
- // the whole of "I do not want to paste blobs any more". What can reach
734
- // this is what a deck somebody here pressed accept on chose to offer.
731
+ // FROM ANY PAIRED DECK, NEITHER AN ADD NOR A HEAL NEEDS MY TICK. Adding
732
+ // is the case the owner asked for by name: an account that appears among
733
+ // the decks I paired with appears on all of them, which is the whole of
734
+ // "I do not want to paste blobs any more". What can reach this is what a
735
+ // paired deck chose to offer.
735
736
  //
736
737
  // A heal used to need the tick, on the reasoning that it replaces a slot
737
738
  // I already have. It replaces only one claude-swap has quarantined, with
@@ -746,13 +747,28 @@ export function createEngine({
746
747
  // so this is the ordinary case between one person's machines, not an
747
748
  // edge.
748
749
  //
749
- // WHICH IS ONLY TRUE OF A DECK SOMEBODY CHOSE. The accept switch presses
750
- // accept for the owner, so "somebody here pressed accept" is not a
751
- // premise about a deck it paired: nobody here chose that deck, and the
752
- // logins this store holds would be whatever it decided to offer. What a
753
- // deck the switch paired offers comes in — added or healed — only for an
754
- // account ticked here, and a person's own press or invite takes the mark
755
- // off its pin (see addTrusted).
750
+ // A DECK THE SWITCH PAIRED INCLUDED, which is what 3.33.0 got wrong. It
751
+ // held such a deck to the tick — what it offered came in only for an
752
+ // account ticked here — on the reasoning that nobody here chose it. On
753
+ // the shipped defaults that was a deadlock rather than a gate. The
754
+ // accept switch ships on, so two of one person's machines pair by it;
755
+ // the setup dialog lists only this deck's own accounts, so a login this
756
+ // deck lacks cannot be ticked here; so an add from such a deck could
757
+ // never pass. A Mac ticked eight working logins, the deck it had paired
758
+ // with by the switch received none of them, ever, and its round line
759
+ // read "all logins fine". The owner chose fully automatic (2026-10-06):
760
+ // whoever made the pairing, a round takes everything plan() returns.
761
+ //
762
+ // What stands between a deck nobody here chose and this store does not
763
+ // rest on the tick, and none of it moved. plan() never replaces a login
764
+ // that works here (syncAction never asks for a forced import). A giving
765
+ // deck hands a deck its switch paired only what a PERSON ticked on it,
766
+ // not what an arrival ticked onward (sharedWith in lan-sync.mjs). And
767
+ // nothing that arrives from one is ticked onward here (ticksOnArrival).
768
+ // What it costs is the switch's own: on a network the owner does not
769
+ // control, a deck the switch pairs can add the logins ticked on it — so
770
+ // the dialog and the README say to turn the switch off there.
771
+ //
756
772
  // OVER `list`, NOT THE RAW ARRAY. `offered`, which keepManifest runs the
757
773
  // list through above (see lan-manifest.mjs), slices to 50 and type-
758
774
  // filters `key` and `email`; this line read `theirs.accounts` and got
@@ -763,15 +779,7 @@ export function createEngine({
763
779
  // the store lock, while the panel drew 50 and every other paired deck
764
780
  // waited behind it. `step.key` also reached transferChallenge and
765
781
  // importAccount untyped, which `offered`'s filter would have caught.
766
- const chosen = () => trustedPeer(cfg.trusted, conn.peerFp)?.auto !== true;
767
- const takes = step => chosen() || cfg.shared.includes(step.key);
768
- const wanted = plan(mine, list).filter(takes);
769
- // TWO CHECKS, BECAUSE THEY END DIFFERENT THINGS. Losing the session —
770
- // LAN switched off, the peer unpaired (`stillPaired`, above) — ends the
771
- // round. From a deck the switch paired, an account unticked mid-round
772
- // ends only that step: the next one may still be ticked, and the skipped
773
- // row says why rather than vanishing.
774
- const stillWanted = takes;
782
+ const wanted = plan(mine, list);
775
783
  /** One login, asked for and opened: a `want` carrying its own proof that
776
784
  * names the account (see transferChallenge), and the `have` opened under
777
785
  * the additional data it was sealed with. The login, or why there is
@@ -790,15 +798,16 @@ export function createEngine({
790
798
  return blob ? { blob } : { why: "could not open" };
791
799
  };
792
800
  let cut = false;
801
+ // Losing the session — LAN switched off, the peer unpaired — ends the
802
+ // round, checked before each question and again before each import. A
803
+ // tick changed here mid-round ends nothing: no step reads one.
793
804
  for (const step of wanted) {
794
805
  if (!stillPaired()) { cut = true; break; }
795
- if (!stillWanted(step)) { done.push({ ...step, ok: false, why: "not shared" }); continue; }
796
806
  const { blob, why } = await wantLogin(step);
797
807
  if (!blob) { done.push({ ...step, ok: false, why }); continue; }
798
- // Unpairing, disabling LAN, or unticking a heal while export was in
799
- // progress takes effect before the received credential touches disk.
808
+ // Unpairing or disabling LAN while export was in progress takes effect
809
+ // before the received credential touches disk.
800
810
  if (!stillPaired()) { cut = true; break; }
801
- if (!stillWanted(step)) { done.push({ ...step, ok: false, why: "not shared" }); continue; }
802
811
  // A verdict rather than a boolean, because "refused" and "kept the
803
812
  // slot it already has" are different things to tell somebody and the
804
813
  // second one used to be reported as success. A bare `true` is still
@@ -825,11 +834,11 @@ export function createEngine({
825
834
  // be asleep or on another network. Nothing new is exposed — the login
826
835
  // came FROM the group, so the group has it.
827
836
  //
828
- // ONLY AN ADD, and only from the local network. A heal already needed
829
- // the tick to happen at all (the filter above), so there is nothing to
830
- // add for one; and a tailnet reaches further than the person's own
831
- // machines, which is a decision they make for themselves rather than
832
- // one an arrival makes for them.
837
+ // ONLY AN ADD, only from the local network, and only from a deck
838
+ // somebody here chose — see ticksOnArrival for why each. A healed slot
839
+ // was here already and its tick is somebody's decision either way; a
840
+ // tailnet reaches further than the person's own machines; and what a
841
+ // deck the switch paired brings is not passed on in this deck's name.
833
842
  // The store can finish an import after the owner disabled LAN or
834
843
  // revoked this peer. Keep the imported slot, but do not turn it into
835
844
  // a newly shared credential on behalf of an obsolete transfer.
@@ -1026,9 +1035,6 @@ export function createEngine({
1026
1035
  const card = id => ({
1027
1036
  ...heardOf(id),
1028
1037
  pairedAt: trustedPeer(cfg.trusted, id)?.at ?? null,
1029
- // And whether a switch said that yes rather than a person, which is
1030
- // what the dialog needs to say which logins will not arrive from it.
1031
- ...(trustedPeer(cfg.trusted, id)?.auto ? { autoPaired: true } : {}),
1032
1038
  });
1033
1039
  for (const p of [...beacon.peers.values(), ...dials.rows()]) {
1034
1040
  if (!stillListed(p, now())) continue;
@@ -536,6 +536,27 @@ export function reachability({ platform, probe, aliases = [], exePath = "", inbo
536
536
  return windowsReach({ probe, aliases, exePath, inbound });
537
537
  }
538
538
 
539
+ /**
540
+ * The verdict as the status route serves it: `said` is the last one taken, and
541
+ * `inboundAt` is when a connection from another machine last got in, as the
542
+ * engine knows it NOW.
543
+ *
544
+ * A VERDICT IS TAKEN AT MOST EVERY FIVE MINUTES (see refreshReach in
545
+ * lan-deck.mjs), and a complaint in it was true only of the moment it was
546
+ * taken. Reported from the Omarchy box this module's Linux block describes:
547
+ * its owner ran the two `ufw allow` lines, decks connected straight away —
548
+ * ufw applies a rule the moment it is added — and the panel went on saying
549
+ * "ufw is running here…" until the cached verdict expired, because it was
550
+ * taken before anything had connected. The connection that got in is the
551
+ * same measurement every platform here already puts above whatever it read,
552
+ * so it outranks a verdict taken before it, too. The answer is the one the
553
+ * next probe will give; every other answer is served as it was.
554
+ */
555
+ export function servedReach(said, inboundAt) {
556
+ if (!said?.blocked || !inboundAt) return said ?? null;
557
+ return { blocked: false, why: "inbound seen", category: said.category ?? "", alias: said.alias ?? "" };
558
+ }
559
+
539
560
  // ── the machine nothing can be asked about ──────────────────────────────────
540
561
  //
541
562
  // Every block above depends on the operating system answering a question.
@@ -10,6 +10,7 @@
10
10
  import { cleanAlias, isAliasKey, lanEnabled, withAlias, withManualEntry } from "./deck-prefs.mjs";
11
11
  import { heldPrefs } from "./prefs-state.mjs";
12
12
  import { lanEngine, lastReach, refreshReach, tailnet } from "./lan-deck.mjs";
13
+ import { servedReach } from "./lan-reach.mjs";
13
14
  import { IDLE_MS as TAILNET_IDLE_MS } from "./tailscale.mjs";
14
15
  import { readBody, send } from "./http-io.mjs";
15
16
 
@@ -21,7 +22,9 @@ export function handleLanStatus(req, res) {
21
22
  // Behind the answer, like the reach probe: the dialog's poll is what finds a
22
23
  // Tailscale somebody installed while the deck was running.
23
24
  if (lanEnabled(heldPrefs.current())) void tailnet.freshen(TAILNET_IDLE_MS);
24
- return send(res, 200, { ok: true, ...lanEngine.status(), reach: lastReach() });
25
+ // Read against what has got in since it was taken — see servedReach.
26
+ const status = lanEngine.status();
27
+ return send(res, 200, { ok: true, ...status, reach: servedReach(lastReach(), status.inboundAt) });
25
28
  }
26
29
 
27
30
  /**
@@ -464,11 +464,12 @@ export function trustedPeer(trusted, fp) {
464
464
  * told nothing changed.
465
465
  *
466
466
  * `auto` SAYS A SWITCH MADE THE PIN, not a person — the accept switch pressing
467
- * accept for the owner. A pin that says so may take what this deck shares and
468
- * may not place logins here: see roundWith in lan-engine.mjs. A person's pin
469
- * of the same key takes the mark away, because pressing accept, or pairing by
470
- * invite, is the choice the switch only stood in for; a switch never puts it
471
- * back on a pin a person made.
467
+ * accept for the owner. A pin that says so is handed only what a person ticked
468
+ * here (see sharedWith), and what it brings is not ticked onward here (see
469
+ * ticksOnArrival in lan-engine.mjs); it places logins here like any paired
470
+ * deck, since 3.38.1 — see roundWith. A person's pin of the same key takes the
471
+ * mark away, because pressing accept, or pairing by invite, is the choice the
472
+ * switch only stood in for; a switch never puts it back on a pin a person made.
472
473
  */
473
474
  export function addTrusted(trusted, entry) {
474
475
  const list = Array.isArray(trusted) ? trusted : [];