Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
3cf76be
Fix licensing gating, cloud failure modes and update hangs for launch
Coding-Dev-Tools Jul 26, 2026
b35a48d
fix(tests): skip FastAPI-dependent launch tests on the numpy-only cor…
Coding-Dev-Tools Jul 26, 2026
b284173
test: make licensing launch coverage portable
Coding-Dev-Tools Jul 26, 2026
09d4482
feat(cloud): carry managed-compute consent on the cloud account
Coding-Dev-Tools Jul 26, 2026
1422ecd
fix(cloud): drop stale grants when the plan changes
Coding-Dev-Tools Jul 26, 2026
e3a23f2
fix(update): make the subprocess budget hold on Windows
Coding-Dev-Tools Jul 26, 2026
8abc7eb
fix(cloud): scope the entitlement cache to its organization
Coding-Dev-Tools Jul 26, 2026
d1f9670
fix(update): bound every step and roll back a failed checkout
Coding-Dev-Tools Jul 26, 2026
eab8a4c
fix(cloud): let a billing denial clear the cached paid access
Coding-Dev-Tools Jul 26, 2026
0cfc1e5
fix(cloud): settle the compatibility cache on a billing denial too
Coding-Dev-Tools Jul 26, 2026
8da56a2
fix(cloud): treat 401 as reconnect, and surface real cloud errors
Coding-Dev-Tools Jul 26, 2026
e3f9f78
fix(cloud): serialize the denial write and distrust an unknown org
Coding-Dev-Tools Jul 26, 2026
6004a43
feat(dashboard): opt-in canvas graph engine behind ?graph-engine=next
Coding-Dev-Tools Jul 26, 2026
23b5ca5
fix(dashboard): lazy-load the graph assets so the CSP stays clean
Coding-Dev-Tools Jul 26, 2026
7802722
Add `engraphis connect --token` so a purchased client can actually co…
Coding-Dev-Tools Jul 26, 2026
2904e98
Read the trial and the access state the cloud sends, and render them
claude Jul 26, 2026
aae7d93
Pre-flight session storage before redeeming the connect token
Coding-Dev-Tools Jul 26, 2026
4336566
fix(graph): address review findings on the opt-in engine
Coding-Dev-Tools Jul 26, 2026
aceb40d
Merge branch 'main' into feat/graph-engine
Coding-Dev-Tools Jul 26, 2026
a4ccdc9
Merge remote-tracking branch 'origin/main' into feat/client-connect
Coding-Dev-Tools Jul 26, 2026
9d77f4f
Address connect review: manifest compute endpoint, timeout and save r…
Coding-Dev-Tools Jul 26, 2026
b20742e
fix(graph): rank communities, settle large galaxy graphs, follow the …
Coding-Dev-Tools Jul 26, 2026
97c538c
test(licensing): make the outage-is-not-a-cancellation test actually …
Coding-Dev-Tools Jul 26, 2026
6084dfd
Report post-redemption connect failures instead of raising tracebacks
Coding-Dev-Tools Jul 26, 2026
385289f
fix(graph): close the remaining review threads on the opt-in engine
Coding-Dev-Tools Jul 26, 2026
038652b
fix(graph): reheat the simulation when a physics slider moves
Coding-Dev-Tools Jul 26, 2026
4e80d62
fix(licensing): settle cached access on entitlement 402
Coding-Dev-Tools Jul 26, 2026
a1b9339
Add `engraphis connect --token` so a purchased client can actually co…
Coding-Dev-Tools Jul 26, 2026
a36b867
Pre-flight session storage before redeeming the connect token
Coding-Dev-Tools Jul 26, 2026
4734128
Address connect review: manifest compute endpoint, timeout and save r…
Coding-Dev-Tools Jul 26, 2026
e971517
Report post-redemption connect failures instead of raising tracebacks
Coding-Dev-Tools Jul 26, 2026
bbcd7c9
Read the trial and the access state the cloud sends, and render them
claude Jul 26, 2026
af9db7c
fix(connect): honor cloud-assigned compute endpoints
Coding-Dev-Tools Jul 26, 2026
8f6e284
fix(graph): trim large graph render costs
Coding-Dev-Tools Jul 27, 2026
a90ae3d
fix(review): address open client release findings
Coding-Dev-Tools Jul 27, 2026
a736884
Merge branch 'feat/graph-engine' into codex/final-release-hardening-2…
Coding-Dev-Tools Jul 27, 2026
c99a1a5
fix: close final release hardening gaps
Coding-Dev-Tools Jul 27, 2026
3d8242e
Add `engraphis connect --token` so a purchased client can actually co…
Coding-Dev-Tools Jul 26, 2026
ca1e118
Pre-flight session storage before redeeming the connect token
Coding-Dev-Tools Jul 26, 2026
972cc7a
Address connect review: manifest compute endpoint, timeout and save r…
Coding-Dev-Tools Jul 26, 2026
3e974c8
Report post-redemption connect failures instead of raising tracebacks
Coding-Dev-Tools Jul 26, 2026
19e89e9
Catch IncompleteRead when draining an error body, cap the timeout, an…
claude Jul 27, 2026
b7caeb4
chore: declare version 1.1.0
Coding-Dev-Tools Jul 27, 2026
b39ac83
feat(dashboard): opt-in canvas graph engine behind ?graph-engine=next
Coding-Dev-Tools Jul 26, 2026
18b865f
fix(dashboard): lazy-load the graph assets so the CSP stays clean
Coding-Dev-Tools Jul 26, 2026
0a65d86
fix(graph): address review findings on the opt-in engine
Coding-Dev-Tools Jul 26, 2026
efa35a6
fix(graph): rank communities, settle large galaxy graphs, follow the …
Coding-Dev-Tools Jul 26, 2026
f32edb2
fix(graph): close the remaining review threads on the opt-in engine
Coding-Dev-Tools Jul 26, 2026
b17ec45
fix(graph): reheat the simulation when a physics slider moves
Coding-Dev-Tools Jul 26, 2026
089f185
perf(graph): apply the classic renderer's large-graph guards to the o…
claude Jul 27, 2026
1e35156
test(graph): exercise the opt-in engine in a real browser
claude Jul 27, 2026
34c4caf
Merge remote-tracking branch 'origin/main' into codex/final-release-h…
Coding-Dev-Tools Jul 27, 2026
86b77e8
Fix the same IncompleteRead escape in cloud_session._post_refresh
claude Jul 27, 2026
9974ae2
Say the token was spent on every post-2xx failure, and verify the ses…
claude Jul 27, 2026
ca97051
Read the trial and the access state the cloud sends, and render them
claude Jul 26, 2026
b76fc69
fix(graph): honor theme and validate lazy assets
Coding-Dev-Tools Jul 27, 2026
dd8258b
Separate connection failures from post-response failures, and fail th…
claude Jul 27, 2026
161ea6c
fix(dashboard): clarify Team access copy
Coding-Dev-Tools Jul 27, 2026
03b04d6
perf(graph): apply the classic renderer's large-graph guards to the o…
claude Jul 27, 2026
8cabe12
test(graph): exercise the opt-in engine in a real browser
claude Jul 27, 2026
6be0909
Fix the same IncompleteRead escape in cloud_session._post_refresh
claude Jul 27, 2026
6d327b2
Say the token was spent on every post-2xx failure, and verify the ses…
claude Jul 27, 2026
4ce33f7
Separate connection failures from post-response failures, and fail th…
claude Jul 27, 2026
43655a7
Require credential fields to actually be strings
claude Jul 27, 2026
12bb132
fix: close latest release review gaps
Coding-Dev-Tools Jul 27, 2026
402542f
Require credential fields to actually be strings
claude Jul 27, 2026
97276e2
chore: declare version 1.1.0
Coding-Dev-Tools Jul 27, 2026
a8c6449
fix(graph): honor theme and validate lazy assets
Coding-Dev-Tools Jul 27, 2026
120634f
fix(graph): restore readable entity labels
Coding-Dev-Tools Jul 27, 2026
6beb34b
feat(graph): serve renderer from v2 dashboard
Coding-Dev-Tools Jul 27, 2026
d4a113a
fix(connect): harden ambiguous device activation failures
Coding-Dev-Tools Jul 27, 2026
daa7035
fix(graph): keep v2 assets lazy and themed
Coding-Dev-Tools Jul 27, 2026
9749af3
fix: absorb current release review heads
Coding-Dev-Tools Jul 27, 2026
04f8b65
fix(cloud): reconnect after malformed refresh status
Coding-Dev-Tools Jul 27, 2026
a472a88
fix(graph): theme labels and release frozen pins
Coding-Dev-Tools Jul 27, 2026
eb248d3
fix(connect): harden ambiguous device activation failures
Coding-Dev-Tools Jul 27, 2026
3914129
feat(graph): serve renderer from v2 dashboard
Coding-Dev-Tools Jul 27, 2026
3cc8003
fix(graph): keep v2 assets lazy and themed
Coding-Dev-Tools Jul 27, 2026
252eb3e
test(cloud): expect reconnect after malformed refresh
Coding-Dev-Tools Jul 27, 2026
e80788f
fix: close remaining client review gaps
Coding-Dev-Tools Jul 27, 2026
d9c2f68
fix(cloud): reconnect after ambiguous refresh transport
Coding-Dev-Tools Jul 27, 2026
537c908
fix(graph): cap node labels by density
Coding-Dev-Tools Jul 27, 2026
df38ff4
test: pin ambiguous refresh timeout handling
Coding-Dev-Tools Jul 27, 2026
29f168b
fix(cloud): reconnect after refresh response reset
Coding-Dev-Tools Jul 27, 2026
24fdef6
fix(graph): bound label rendering density
Coding-Dev-Tools Jul 27, 2026
5c8644e
Merge branch 'codex/final-release-hardening-20260726' into codex/cons…
Coding-Dev-Tools Jul 27, 2026
13b03f0
Merge remote-tracking branch 'origin/launch/payments-licensing-harden…
Coding-Dev-Tools Jul 27, 2026
039c249
Merge remote-tracking branch 'origin/feat/client-connect' into codex/…
Coding-Dev-Tools Jul 27, 2026
adfabfd
Merge remote-tracking branch 'origin/feat/graph-engine' into codex/co…
Coding-Dev-Tools Jul 27, 2026
3795297
Merge remote-tracking branch 'origin/fix/trial-and-plan-display' into…
Coding-Dev-Tools Jul 27, 2026
d8f7b91
Merge remote-tracking branch 'origin/chore/version-1.1.0' into codex/…
Coding-Dev-Tools Jul 27, 2026
472a592
Merge branch 'feat/client-connect' into codex/consolidate-engraphis-m…
Coding-Dev-Tools Jul 27, 2026
da70894
Merge branch 'fix/trial-and-plan-display' into codex/consolidate-engr…
Coding-Dev-Tools Jul 27, 2026
d0e86f0
fix: reconcile release branch merge conflicts
Coding-Dev-Tools Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 20 additions & 29 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,6 @@ All notable changes to Engraphis are documented here. Format loosely follows

Public 1.1.0 hosted-connect and graph-experience release.

### Removed

- **"Signed compliance export" is no longer advertised on any surface.** It was granted to
Pro and Team by the control plane's plan→feature table and promoted in the account portal,
the dashboard upgrade panel, this changelog, and the README pricing table — but it was
never implemented on either side: no signing code in this client, and no export route, no
`export:*` token scope, and no export job kind in Engraphis Cloud. The `export` key is
removed from the plan→feature tables in both repos and from the dashboard's entitlement
vocabulary.
- **No entitlement behaviour changes.** Those tables are read only to decide which lock
badges and plan bullets to draw; every paid operation was, and still is, authorized by
the cloud's token scopes and paid-entitlement dependencies.
- **Local workspace export is unaffected and always was free.** `GET /export` returns the
full bi-temporal dump (memories, sessions, audit) on every plan and is deliberately not
entitlement-gated so data portability survives recovery mode. It is now listed in the
README as the free capability it is.
- `GET /export?signed=true` and `GET /analytics/export` still answer `501`, but now say
the capability is not implemented and name the working alternative, instead of implying
it is available in Engraphis Cloud.

### Added

- **`engraphis connect --token engr_ct_…`** — the missing client half of device connect.
Expand All @@ -54,9 +34,26 @@ Public 1.1.0 hosted-connect and graph-experience release.
- A stable per-installation identity at `~/.engraphis/client_identity.json` (random ULIDs,
not a hardware fingerprint) so reconnecting a machine updates its existing installation
instead of registering a new device every time.
- An opt-in canvas graph engine (`?graph-engine=next`) with the existing ForceGraph + D3
visual modes, deterministic communities, accessible classic fallback, lazy CSP-confined
assets, bounded simulations, and large/dense-graph rendering guards.

### Removed

- **"Signed compliance export" is no longer advertised on any surface.** It was granted to
Pro and Team by the control plane's plan→feature table and promoted in the account portal,
the dashboard upgrade panel, this changelog, and the README pricing table — but it was
never implemented on either side: no signing code in this client, and no export route, no
`export:*` token scope, and no export job kind in Engraphis Cloud. The `export` key is
removed from the plan→feature tables in both repos and from the dashboard's entitlement
vocabulary.
- **No entitlement behaviour changes.** Those tables are read only to decide which lock
badges and plan bullets to draw; every paid operation was, and still is, authorized by
the cloud's token scopes and paid-entitlement dependencies.
- **Local workspace export is unaffected and always was free.** `GET /export` returns the
full bi-temporal dump (memories, sessions, audit) on every plan and is deliberately not
entitlement-gated so data portability survives recovery mode. It is now listed in the
README as the free capability it is.
- `GET /export?signed=true` and `GET /analytics/export` still answer `501`, but now say
the capability is not implemented and name the working alternative, instead of implying
it is available in Engraphis Cloud.

### Changed

Expand All @@ -67,12 +64,6 @@ Public 1.1.0 hosted-connect and graph-experience release.
`ENGRAPHIS_MANAGED_COMPUTE_CONSENT` remains as an explicit operator override (`=0` opts a
connected installation back out, `=1` forces it on regardless of session state) and is no
longer surfaced anywhere in the UI.
- Hosted plan presentation now follows the control plane's active, trial, expired, and
lapsed states; renewal follows the plan actually held, account actions use the
plan-neutral portal, and a spent trial is never offered again.
- Device-connect and refresh responses validate credential fields as strings, preserve
single-use-token semantics across truncated responses, and refuse unsafe session-storage
paths before redeeming a token.

## [1.0.1] - 2026-07-24

Expand Down
100 changes: 34 additions & 66 deletions engraphis/cloud_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,6 @@ def _reachable_cloud_base_url(value: str) -> str:
raise CloudSessionError("Engraphis Cloud is temporarily unreachable.") from exc


def _server_compute_url(response: dict) -> str:
"""Return the refresh response's compute endpoint only when it is safe to persist."""

value = response.get("compute_url")
if not isinstance(value, str) or not value.strip():
return ""
try:
return validate_cloud_base_url(value)
except (CloudUrlUnresolved, ValueError):
# This optional field cannot be allowed to clear or poison a known-good endpoint.
# The rotated credential is still persisted below, so a later valid response can
# repair a session whose compute endpoint was not supplied yet.
return ""


def _session_path() -> Path:
root = os.environ.get("ENGRAPHIS_STATE_DIR", "").strip()
base = Path(root).expanduser() if root else Path.home() / ".engraphis"
Expand Down Expand Up @@ -525,8 +510,7 @@ def text_field(response: dict, key: str) -> str:


def save_bootstrap(response: dict, *, control_url: str,
compute_url: Optional[str] = None,
compute_url_source: Optional[str] = None) -> None:
compute_url: Optional[str] = None) -> None:
"""Persist the one-time bootstrap/refresh material returned by the control plane."""

refresh = text_field(response, "refresh_credential")
Expand All @@ -547,11 +531,6 @@ def save_bootstrap(response: dict, *, control_url: str,
response.get("token_subject") or "member"
),
}
if compute_url_source in {"explicit", "server", "fallback"}:
# Keep an explicit CLI choice distinct from a server/distribution default. A
# refresh may move cloud-assigned endpoints, but must never silently replace an
# endpoint the operator deliberately selected.
value["compute_url_source"] = compute_url_source
# Whatever entitlement the control plane volunteered, so the dashboard knows the plan
# from the first boot instead of inferring it. Absent on an older cloud; harmless.
value.update(_declared_entitlement(response))
Expand Down Expand Up @@ -595,13 +574,6 @@ def _refresh_http_error(status: int) -> CloudSessionError:
#: :func:`_post_refresh`; ``engraphis.device_connect`` guards its drain with the same tuple.
_DRAIN_FAILURES = (OSError, ValueError, http.client.HTTPException)

#: Public copy for every ambiguous refresh response. Once the POST is written, replaying the
#: single-use credential can revoke its family; reconnecting is the safe recovery.
_INCOMPLETE_REFRESH_RESPONSE = (
"Engraphis Cloud answered this session refresh but the reply was incomplete, "
"so the rotated credential could not be saved. Connect this installation again."
)


def _post_refresh(control_url: str, refresh: str, workspace_id: Optional[str],
token_subject: str) -> dict:
Expand Down Expand Up @@ -649,21 +621,35 @@ def _post_refresh(control_url: str, refresh: str, workspace_id: Optional[str],
except _DRAIN_FAILURES:
pass
raise _refresh_http_error(code)
# urllib wraps failures before or while sending the request in URLError. That is the one
# distinguishable pre-send path, so the credential was not spent and retry remains safe.
except urllib.error.URLError as exc:
raise CloudSessionError("Engraphis Cloud is temporarily unreachable.") from exc
except (TimeoutError, OSError, http.client.HTTPException) as exc:
# These failures arise while ``open`` is waiting for the response, after urllib has
# written the POST. The control plane may have spent the single-use credential even
# though no usable status line reached us.
#
# Keep ``URLError`` separate above: urllib uses it for failures while establishing or
# sending the request, the only phase where retrying the same credential is safe.
# ``RemoteDisconnected`` has both OSError and BadStatusLine ancestry and therefore
# deliberately lands in this non-transient bucket.
except (TimeoutError, http.client.RemoteDisconnected, OSError) as exc:
# These escape directly from getresponse() after urllib wrote the POST. The control
# plane may have spent the single-use credential even though no status line arrived;
# retrying the unchanged on-disk value risks revoking its entire credential family.
raise CloudSessionError(
"Engraphis Cloud did not complete this refresh response, so the rotated "
"credential could not be saved. Connect this installation again.",
status=409,
) from exc
except (http.client.BadStatusLine, http.client.LineTooLong) as exc:
# ``getresponse()`` raises these only after urllib has sent the POST. The control
# plane may therefore have consumed the single-use refresh credential, while its
# replacement never reached disk. Retrying would replay the stale credential and can
# revoke its whole family, so prefer reconnecting over an unsafe transient retry.
# ``RemoteDisconnected`` is also a ``BadStatusLine`` but reaches the earlier OSError
# transport clause through its ``ConnectionResetError`` base.
raise CloudSessionError(
_INCOMPLETE_REFRESH_RESPONSE,
"Engraphis Cloud returned a malformed refresh response, so the rotated "
"credential could not be saved. Connect this installation again.",
status=409,
) from exc
except http.client.HTTPException as exc:
# Other malformed HTTP replies have no useful protocol status, but unlike a malformed
# status line they do not establish that this request reached the control plane.
raise CloudSessionError("Engraphis Cloud is temporarily unreachable.") from exc
Comment on lines +649 to +652

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat post-send HTTP protocol errors as ambiguous refreshes

When getresponse() raises another HTTPException, such as UnknownProtocol after receiving an HTTP/2.0 status line, the refresh POST has already been sent and may have consumed the single-use credential. This handler instead returns the default 503, which callers present as transient and invites retrying the unchanged on-disk credential; if the first request rotated it, that replay can revoke the credential family. These post-send protocol failures need the same non-transient reconnect handling as BadStatusLine and LineTooLong.

Useful? React with 👍 / 👎.


try:
with response:
Expand All @@ -682,7 +668,12 @@ def _post_refresh(control_url: str, refresh: str, workspace_id: Optional[str],
# happened before the server committed the rotation the old credential was still
# good and the reconnect was unnecessary, but the opposite mistake revokes every
# credential in the family and forces the same reconnect anyway, from a worse state.
raise CloudSessionError(_INCOMPLETE_REFRESH_RESPONSE, status=409) from exc
raise CloudSessionError(
"Engraphis Cloud answered this session refresh but the reply was incomplete, "
"so the rotated credential could not be saved. Connect this installation "
"again.",
status=409,
) from exc

# These are post-response too, so they carry the same replay hazard as the truncated
# body above and take the same non-transient status: the server consumed the credential
Expand Down Expand Up @@ -753,9 +744,7 @@ def access_for_workspace(
# response; a stale home-directory mount yields a structured, retryable error from
# ``_load`` rather than an unhandled filesystem exception. The authoritative session
# record is still loaded again under the lock below before any credential is used.
# A refresh can now supply a missing compute endpoint, so do not reject a valid saved
# control/refresh session before it has a chance to receive that authoritative value.
if not configured(require_compute=False):
if not configured(require_compute=require_compute):
raise CloudSessionError(
"Connect this installation to Engraphis Cloud first.", status=401
)
Expand All @@ -771,18 +760,8 @@ def access_for_workspace(
).strip()
control = os.environ.get("ENGRAPHIS_CLOUD_CONTROL_URL", "").strip()
control = control or str(saved.get("control_url") or "").strip()
saved_compute = str(saved.get("compute_url") or "").strip()
compute = direct_compute or saved_compute
compute_source = "explicit" if direct_compute else str(
saved.get("compute_url_source") or ""
)
if saved_compute and compute_source not in {"explicit", "server", "fallback"}:
# Pre-source sessions cannot tell a cloud-assigned endpoint from a CLI/env
# override. Preserve the nonempty value rather than silently moving a
# potentially operator-selected endpoint; compute-less legacy sessions still
# accept a vetted response below.
compute_source = "explicit"
if not refresh or not control:
compute = direct_compute or str(saved.get("compute_url") or "").strip()
if not refresh or not control or (require_compute and not compute):
raise CloudSessionError(
"Connect this installation to Engraphis Cloud first.", status=401
)
Expand All @@ -808,10 +787,6 @@ def access_for_workspace(
response_subject = _validated_token_subject(
body.get("token_subject") or token_subject
)
server_compute = _server_compute_url(body)
if server_compute and compute_source != "explicit":
compute = server_compute
compute_source = "server"
updated = dict(saved)
updated.update({
"schema": "engraphis-cloud-session/v1",
Expand All @@ -822,8 +797,6 @@ def access_for_workspace(
"refresh_expires_at": text_field(body, "refresh_expires_at"),
"token_subject": response_subject,
})
if compute_source in {"explicit", "server", "fallback"}:
updated["compute_url_source"] = compute_source
# The refresh response carries the same entitlement fields as registration, so the
# plan re-confirms itself on every token rotation. An older cloud omits them and
# the previously persisted answer (if any) is left untouched.
Expand All @@ -845,9 +818,4 @@ def access_for_workspace(
updated.pop(key, None)
updated.update(declared)
_save(updated)
if require_compute and not compute:
raise CloudSessionError(
"Engraphis Cloud did not provide a compute endpoint for this installation.",
status=503,
)
return access, organization_id, compute
24 changes: 10 additions & 14 deletions engraphis/dashboard_assets/engraphis-graph.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
/* Engraphis knowledge graph — the unified v2 dashboard's opt-in force-graph engine.
`engraphis.dashboard_app` owns this renderer and serves it from `/v2-assets`; the legacy
server exposes only the compatibility marker in `engraphis/static/engraphis-graph.js`.

/* Engraphis knowledge graph — the dashboard's opt-in force-graph engine.
Restores the shipped behaviour: GRAPH_PRESETS, GSTYLE render modes (cyber/galaxy/solar/classic),
STYLE_PAL / STYLE_LAYERS / STYLE_BG, COMMUNITY_PALS, GRAPH_HEAT, colour-by community/type/connections,
GRAPH_PALETTES with per-entity-type overrides, d3 force wiring, directional particles, label ranking,
Expand Down Expand Up @@ -312,8 +309,8 @@
path: null, asOf: null, ghost: true, sizeBy: 'degree', bridges: false, suggestions: false, collapse: 'auto'
};
let raw = { nodes: [], links: [], suggestions: [] }, adj = {}, hilite = null, hoverSet = null, maxDeg = 1;
// Match the classic renderer's ranked label cap. Computing the selected IDs once per
// render also keeps per-frame canvas work bounded on dense graphs.
// The classic renderer treats label density as a hard ranked cap, not merely a looser
// degree threshold. Keeping chosen IDs outside the paint callback bounds fillText work.
let labelIds = new Set();
let zoom = 1, collapsed = false;
/* Recomputed from the *rendered* data on every render, exactly as the classic path
Expand Down Expand Up @@ -640,14 +637,14 @@
// The dashboard setting is a screen-space font size. As on the classic renderer,
// compensate only for graph zoom; an extra artistic divisor makes a configured 12px
// label unreadable at normal zoom and makes the Font size control misleading.
const size = state.settings.font / scale;
const size = Math.max(2, state.settings.font / scale);
ctx.font = '500 ' + size + 'px system-ui, sans-serif';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'rgba(0,0,0,.5)';
ctx.fillText(nodeName(node), node.x + r + 1.6 + 0.3, node.y + 0.3);
// The canvas sits on both light and dark themes. A hard-coded pale label disappears
// on the light canvas, including for a highlighted node.
ctx.fillStyle = state.themeColors.label || '#e7e9ee';
// Node names sit directly on the canvas, so Classic's light and sepia themes need
// the dashboard-resolved text colour just like relation and collapsed-cluster labels.
ctx.fillStyle = state.themeColors.label || (node.id === hilite ? '#ffffff' : 'rgba(232,236,245,.86)');
ctx.fillText(nodeName(node), node.x + r + 1.6, node.y);
}
ctx.globalAlpha = 1;
Expand Down Expand Up @@ -940,11 +937,10 @@
api.freeze = on => {
state.settings.frozen = on;
if (on) { fg.d3Force('charge').strength(0); fg.d3AlphaDecay(1); return; }
applyForces();
// Dragging pins nodes with fx/fy. "Unfreeze" must release those pins even when reduced
// motion suppresses the reheat below; otherwise the control claims movement is restored
// while every dragged node remains immobile.
// Dragging pins a node with fx/fy. Unfreezing is a request to resume the layout, not
// merely the unpinned subset, so release those anchors before the simulation reheats.
raw.nodes.forEach(n => { n.fx = undefined; n.fy = undefined; });
applyForces();
if (reduced()) return;
fg.d3AlphaDecay(alphaDecay());
if (fg.d3ReheatSimulation) fg.d3ReheatSimulation();
Expand Down
Loading
Loading