feat(devices): one device name everywhere — nickname first (CU-86ake4xz3) - #386
Open
bohdan-blip wants to merge 11 commits into
Open
bohdan-blip wants to merge 11 commits into
bohdan-blip wants to merge 11 commits into
Conversation
…xz3) Every screen that shows or searches a device name now resolves it through `getDeviceName` (nickname -> displayName -> hostname). 25 call sites built that name with their own `displayName || hostname` chain, so a device rendered under its nickname could not be found by typing it. - `nickname` moves DOWN the fragment ladder, from `deviceFields_machine` (step 3) to `deviceRowFields_machine` (step 1). Selected whole at one step, per the ladder's rule. This is what makes the schedule picker and the Assigned Devices tab resolve the same name as the Devices page. The raw-POST assignments path is type-checked against the generated row shape, so it had to start selecting the field too. - Client-side search predicates share one primitive, `matchesDeviceName`: the rendered name OR the hostname, case-insensitive substring. Hostname stays a match after a nickname is set on purpose — the server's `devices(search:)` matches hostname regardless of nickname (alongside ip, serial, manufacturer, model; openframe-oss-lib#1658), and a list filtering what it already loaded must not be stricter than the page it stands in for. Before, the three predicates had three different rules. - Logs carry `nickname` as of openframe-oss-lib#2088 / openframe-saas- tenant#3020. `LogEvent`/`LogDetails` have no `displayName`, so the synthetic device the log row builds stops inventing one out of the hostname and carries the real field instead. Log details keep fetching the live device for their card, so the snapshot field is not selected there. - `remote-shell` / `remote-desktop` / `file-manager` had the priority inverted (hostname first); their `hostname` variable only ever fed a page title and a Mingo label, so it is renamed to `deviceName`. - First test coverage for `getDeviceName` and `matchesDeviceName`, including the empty-string fall-through that Pinot's schema default for the logs column produces. `schema.graphql` is hand-edited with the two `nickname: String` fields exactly as openframe-oss-lib#2088 adds them to `log.graphqls`; verified field-for-field against test-3020. Re-run `npm run fetch-schema` once that PR is deployed to a normal environment. Tickets keep their own name chain: they read machines over `/chat/graphql` (saas-ai-agent), whose `Machine` exposes only id, machineId, hostname and organizationId — verified by introspecting all 160 types on test-3020. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
…iceName (CU-86ake4xz3)
The tickets list Source column and the ticket details header/Device field were the
last device-name surfaces still on hostname: their machine comes from /chat/graphql,
whose Machine type had no nickname until openframe-saas-tenant#3020 added it
(resolved live from the device registry, nullable).
Select `nickname` in the three ClientTicketOwner.machine selections and route both
surfaces through one helper, getTicketDeviceName = getDeviceName({ nickname,
hostname: deviceHostname || machine.hostname }), so tickets follow the same
nickname -> displayName -> hostname rule as every other screen. Drop the
Machine.displayName field the types carried: the chat schema never had it and no
query selected it, so both fallbacks that read it were dead.
Depends on the ai-agent change being deployed — a tenant whose ai-agent predates it
fails the whole tickets query on the unknown field.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fleet audit events (User logged in, Created saved query) arrive with the string "null" — not null — in hostname, organizationName and organizationId. The Source cell already mapped that hostname to "System" but rendered the organization as-is, so every system row read "System / null". Map the organization the same way, on the logs page and the onboarding logging step. The backend is being fixed at ingest, but log rows are immutable, so the rows already written keep the literal — the guard stays for them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…name-ssot # Conflicts: # src/app/(app)/devices/details/file-manager/page.tsx # src/app/(app)/devices/details/remote-desktop/page.tsx
…on machineId presence Both owner kinds carry the `type` discriminant, so check that instead of whether a `machineId` field happens to exist — an ADMIN owner that ever grows one would no longer be misread as a CLIENT owner with a missing machine. Review follow-up on #386. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (CU-86ake4xz3) getDeviceName accepted a shape whose keys were all optional, so an operation that forgot to select `nickname` compiled and quietly rendered the hostname — the silent-fallback trap the fragment ladder in src/graphql/devices/ exists to close (its own header describes the all-optional MachineLike it replaced). Close it at the sink too: - `DeviceNameSource` requires the `nickname` key (null/undefined allowed, silence not); `Device.nickname` and `DeviceGraphQlNode.nickname` become required keys, and `PartialNamedDevice` is the Partial<Device> that still carries it (log rows, DeviceInfoSection). A test pins the contract with @ts-expect-error so loosening it fails tsc. - execution-helpers: `MachineLike`, the hand-written all-optional interface, becomes `ExecutionMachine`, typed off the generated executionFields machine. - monitoring rows: the field named `displayName` held the resolved name, and the query table searched it through matchesDeviceName on a row that is a Fleet host, not a device. Rename it `name` and search name/hostname explicitly. - LogEntry.nickname is `string | null`, as the query returns it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e selector search once The remote-desktop and remote-shell pages kept a useMemo around a plain string; useTrackOpenView already depends on the primitive fields, and the compiler memoizes the rest. The device selector trimmed the term for the name clause but not for the OS clause — one needle for both. The useTrackOpenView example named the device by hostname; it names it like every other screen now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rals The logs page and the onboarding logging step each mapped the literal "null" a system event carries — first the name to "System", then (since the previous commit) the organization to empty — in their own cell renderer. One helper, tested, used by both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
🦩 Flamingo Code Review3 finding(s) — 0 action required · 3 recommended · 0 informational Mode: advisory · 3 defect(s) outside any rule Inline comments: 2 new Findings without an inline anchor in this diff
Need another pass? Commits pushed after this review are not reviewed automatically.
Prefer typing? Comment React 👍/👎 on inline comments to teach the reviewer. Started 2026-09-15 15:01 UTC · updated 2026-09-15 15:02 UTC · workflow run |
Like unreadNotificationCount, the field rides no feature flag, so the ai-agent that added it (openframe-saas-tenant#3020) has to ship before this frontend — say so next to the selection, where the constraint for the other field lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A device renamed by the user (
nickname, labelled Display Name in the UI) still showed its hostname on most screens, and could not be found by typing that name. On test-3020 the only device ishostname: "Mac",displayName: null,nickname: "MyMacTest1": the Devices page said MyMacTest1, the Run Script picker said Mac, the Logs SOURCE column said Mac, and searching the picker for the device's own name returned "No devices found".Every screen that shows or searches a device name now resolves it through the existing SSOT
getDeviceName()(nickname → displayName → hostname), and every client-side search predicate shares one primitive. 25 hand-writtendisplayName || hostnamechains removed.ClickUp: 86ake4xz3. Backend: openframe-oss-lib#2088, openframe-saas-tenant#3020.
Changes
nicknamemoves fromdeviceFields_machine(step 3) down todeviceRowFields_machine(step 1), selected whole at one step per the ladder's rule. This is what makes the schedule picker and the Assigned Devices tab resolve the same name as the Devices page; before,getDeviceNamethere silently fell through todisplayName. The raw-POST assignments union query is type-checked against the generated row shape and had to start selecting the field too.device-selector.tsxname cell andtest-script-modal.tsxcallback →getDeviceName. One cell fixes eight pickers.matchesDeviceName(device, search)besidegetDeviceName: the rendered name or the hostname, case-insensitive substring. Hostname stays a match after a nickname is set on purpose: the server'sdevices(search:)matches hostname regardless of nickname (alongside ip, serial, manufacturer, model —CustomMachineRepositoryImpl.buildDeviceQuery), and a list filtering what it already loaded must not be stricter than the page it stands in for. Before, the three client predicates had three different rules. Used by the pickers,DevicesAssignedTable, and the monitoring Query "Assigned Devices" table.machineLabel(all Execution History tables + execution details), schedule assign/unassign toasts, sharedDeviceInfoSectionandLogDrawerdevice cards (also drop thedescription/machineIdfallbacksdevice-name.tsforbids), monitoring query/policy row builders, live-query device dropdown, ticket device autocomplete, assignments type-ahead label.hostname || displayName). Theirhostnamevariable only ever fed a page title and the Mingo "open view" label, so it is renameddeviceName; theFileManagerContainerprop follows, since a nickname now travels through it.LogEvent.nickname/LogDetails.nickname(new on the backend) selected in the Relay fragment and inGetLogs; the synthetic device the log row builds stops inventingdisplayNameout of the hostname and carries the real field.GetLogDetailsdeliberately does not select it: that page's device card fetches the live device, and a snapshot name there would be wrong.schema.graphql— the twonickname: Stringfields hand-added exactly as #2088 adds them tolog.graphqls; verified field-for-field (including position) by introspecting test-3020.getDeviceName(7) andmatchesDeviceName(7), including the empty-string fall-through that Pinot's schema default for the logs column produces.Testing
npm run relay,npm run type-check,npm run lint:ci, Prettier — green.npm run buildgreen before the rebase onto currentmain.main(+2 unrelated commits) reported 1 failure in 206; every subset re-run since (src/lib61,src/graphql/notifications21,mobile-auth-return8) passes, and the full suite was not re-run to keep the machine free.npm testis not a CI gate. To be pinned down before merge.Machine.nickname,LogEvent.nickname,LogDetails.nicknamepresent;devices(search:)finds the device by nickname, by substring, lowercase, and by hostname;logs(search:"MyMac")→ 3 RMM events carryingnickname;scriptExecutions[].machine.nicknamepopulated; all four raw-POST documents in this PR (AssignmentsAssignedItems,AssignmentsDevicesSearch,GetLogs,GetLogDetails) executed verbatim against the live schema — no validation errors.getDeviceName); Logs search "MyMac" → 3 rows, SOURCE column "Mac"; Run Script picker → "Mac", search "MyMac" → "No devices found"; Execution History → "Mac" while the API returnsnickname: "MyMacTest1"for the same rows.relay/unused-fields(editor-only backlog, off ineslint.ci.mjs): +2 net, all inside the fragment definition files;logs-table.tsxstays clean becausenode.nicknameis read directly.Backend gaps — deliberately not worked around here
/chat/graphql(saas-ai-agent)Machineexposes onlyid, machineId, hostname, organizationId— verified by introspecting all 160 types. A client-side join through/api/graphql'sdevice(machineId:)is possible but rejected: it would be a string-built document (raw-POST against/api/graphql, which CLAUDE.md forbids), a cross-service join on the client, and would mix a live nickname with the snapshottedTicket.deviceHostname. Fix isTicket.deviceNicknamenext todeviceHostname.logs(search:"Mac")→ 0 for events whosehostnameis"Mac"; controllogs(search:"executed")→ 1. Blocks the fallback case: a device without nickname shows its hostname in Logs but cannot be searched by it.hostnameas the literal string"null", nodeviceId, nonickname. The existing'null' → "System"guard in the logs table still applies unchanged.Notes
schema.graphql, not the runtime. Deployed to an environment withoutLogEvent.nickname, the wholelogsquery errors and the Logs page breaks. #3020 currently has conflicts and no review.schema.graphqlis hand-edited. Re-runnpm run fetch-schemaonce #2088 reaches a normal environment; running it against one without the field will silently dropnicknameand break the build on the logs fragment.MyMac → MyMacTest1on the stand, the 3 old events still readMyMacwhile every live surface (Devices, picker, Execution History, the device card in the log drawer) readsMyMacTest1. Expected, not a bug — worth knowing before QA files it as one.npm run devdoes not run relay-compiler andsrc/__generated__is gitignored: after checking out this branch runnpm run relay, or the stale artifacts will not selectnicknameand the old names come back.DEVICE_LIST_LIMIT, pre-existing, affects hostname search equally);MachineLikeinexecution-helpers.tsis the hand-written structural type CLAUDE.md warns about and wants a small@inlinemachine fragment;logs-table.tsx/logging-step.tsxbuild/log-details?…by hand outsideroutes.ts.🤖 Generated with Claude Code