patch(activity backend): record device name/IP/MAC on per-miner bulk action detail#71
Conversation
At first write, the Upsert query joins device + discovered_device and freezes the composed display name (custom_name, else manufacturer + model), IP, and MAC onto the log row. The ON CONFLICT branch leaves the snapshot untouched so retries and the reaper never overwrite first-write values. Exposed as optional fields on CommandBatchDeviceResult so the activity log detail view can render human-recognizable identifiers without re-resolving against mutable current state (names can be renamed, IPs are DHCP). Historical rows (pre-migration) surface as unset optionals; the frontend falls back to the device UUID. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an immutable “device identity snapshot” (name/IP/MAC) to per-miner bulk action results so operators can audit historical batches without cross-referencing UUIDs.
Changes:
- Add nullable snapshot columns to
command_on_device_logand populate them on first write viaUpsertCommandOnDeviceLog. - Extend
GetCommandBatchDeviceResultsRPC to return optionaldevice_name,ip_address, andmac_address. - Add integration tests covering snapshot immutability and backward compatibility for pre-migration rows.
Reviewed changes
Copilot reviewed 6 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| server/sqlc/queries/command.sql | Populates snapshot fields on first upsert and returns them in batch result listing |
| server/migrations/000037_add_device_snapshot_to_command_on_device_log.up.sql | Adds nullable device snapshot columns to command_on_device_log |
| server/migrations/000037_add_device_snapshot_to_command_on_device_log.down.sql | Drops the snapshot columns on rollback |
| server/internal/domain/command/service.go | Maps new nullable DB fields to optional proto fields in the RPC response |
| server/internal/domain/command/results_integration_test.go | Adds integration tests for snapshot immutability + historical NULL behavior |
| proto/minercommand/v1/command.proto | Adds optional device_name, ip_address, mac_address fields to CommandBatchDeviceResult |
| server/generated/sqlc/models.go | sqlc regeneration to include new model fields |
| server/generated/sqlc/command.sql.go | sqlc regeneration for updated queries/row structs |
| server/generated/grpc/minercommand/v1/command.pb.go | Regenerated Go protobuf for new optional fields |
| client/src/protoFleet/api/generated/minercommand/v1/command_pb.ts | Regenerated TS protobuf for new optional fields |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0896d3c15
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
🔐 Codex Security Review
Review SummaryOverall Risk: LOW FindingsNo concrete security, correctness, or reliability defects stood out in the reviewed diff. Notes
Generated by Codex Security Review | |
Single source of truth for the miner display-name rule (custom_name, else manufacturer + " " + model). Replaces an inline conditional in the live fleet read path; other consumers can now call it instead of reimplementing the fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capture custom_name / manufacturer / model / ip_address / mac_address on command_on_device_log instead of a pre-composed device_name. The read path composes the display name via fleetmanagement.ComposeDeviceName so the audit log stays in lockstep with the live fleet read. The UpsertCommandOn- DeviceLog query is now rendering-free; the proto doc comment describes intent rather than the implementation recipe. Wire-level proto shape and frontend expectations are unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The comments on the command_on_device_log snapshot columns paired "at command-completion time" with "when the action ran" / "Reflects state at action-time", which read as enqueue-time. In fact the capture happens on the first terminal write (worker result or reaper timeout), so for a long-running command a rename / DHCP change between enqueue and that write is recorded here, not the pre-enqueue identity. Tightens the proto field docs, migration header, UpsertCommandOnDeviceLog query comment, and DeviceSnapshot test doc. Generated .pb.go / _pb.ts / sqlc Go files follow. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Made-with: Cursor
…ce info Switch from chevron-based inline expand to a click-to-open modal for activity detail. Every activity row is now clickable, showing a summary (event, timestamp, scope, user, result) and — for batch actions — the per-miner device results table with name, IP, and MAC from PR #71. - Add ActivityDetailModal with summary section + StatusCircle indicator - Remove ActivityBatchDetails (superseded by modal) - Collapse initiated + completed batch rows; fix count mismatch - Strip "completed" from descriptions in the list view - Extract isCompletedEvent/baseEventType to shared eventType utility - Add fetchedRef to useCommandBatchDeviceResults to skip redundant RPCs - Stabilize onDismiss callback and drop derived isPending prop Made-with: Cursor
Problem
PR #25 shipped the per-miner batch detail RPC, but each row returns only the device UUID — a value the app surfaces nowhere else. Operators auditing a past bulk action have no way to recognize which miners were affected without cross-referencing UUIDs against another system. Frontend PR #60 renders whatever identifiers the backend hands it; today that's just the UUID.
What ships
Backend only. Follows the write-time capture convention already used by
ActivityEntry.scope_label. Closes nothing; companion to #22 / #60.command_on_device_log(custom_name,manufacturer,model,ip_address,mac_address), populated byUpsertCommandOnDeviceLogjoiningdevice+discovered_device. The read path composes the display name viafleetmanagement.ComposeDeviceName— the same helper the live fleet read path uses, so the audit log and live UI never disagree on what a miner is called.ON CONFLICTleaves the captured columns untouched so retries and the reaper-after-success path preserve the original audit values — even if the device was renamed or moved IPs between the first and second Upsert.CommandBatchDeviceResult.{device_name, ip_address, mac_address}areoptional string; historical rows (pre-migration) surface as unset so the frontend can fall back to UUID.Where to review
Schema
server/migrations/000037_add_device_snapshot_to_command_on_device_log.up.sql— five nullableTEXTcolumns. Nullable so historical rows stay NULL and the frontend can detect them.SQL
server/sqlc/queries/command.sql—UpsertCommandOnDeviceLoghas adevCTE that copiesdevice.custom_name/device.mac_address/discovered_device.{manufacturer, model, ip_address}verbatim; no composition or rendering happens in SQL.ON CONFLICT DO UPDATEintentionally excludes the captured columns.ListBatchDeviceResultsselects the five new columns.Shared helper
server/internal/domain/fleetmanagement/device_name.go—ComposeDeviceName(customName, manufacturer, model string) stringis the single source of truth for the display-name rule. Bothfleetmanagement/service.go(live fleet read) andcommand/service.go(audit log read) call it; the live-fleet call site was refactored to use it too so there's nowhere for the rule to drift.Proto
proto/minercommand/v1/command.proto— threeoptional stringfields onCommandBatchDeviceResult(tags 5/6/7).optionalis deliberate so the frontend can distinguish "never captured" (historical) from "captured empty." The field doc describes the consumer contract, not the composition recipe.Service
server/internal/domain/command/service.go— mapper reads the five raw sqlc fields, composesdevice_nameviafleetmanagement.ComposeDeviceName, and passesip_address/mac_addressthrough using the existingErrorMessageNullString-to-pointer pattern.Tests
server/internal/domain/fleetmanagement/device_name_test.go— table-driven unit test forComposeDeviceNamecovering the five relevant cases (custom set, custom empty with both mfr/model, only mfr, only model, all empty).server/internal/domain/command/results_integration_test.go—TestGetCommandBatchDeviceResults_DeviceSnapshotcovers the full invariant: first Upsert captures the fields; a rename + IP change; second Upsert (reaper-style) flips status/error_info but leaves the captured values alone.TestGetCommandBatchDeviceResults_HistoricalRowsOmitSnapshotcovers backward compatibility by inserting a codl row directly with NULL captured columns and asserting the proto optionals surface as unset.Test plan
cd server && just testpasses (the two new integration tests run against the TimescaleDB container and should both pass)/miners/:id→ rename) and expand the same batch again — original name still shown (audit invariant)INSERT INTO command_on_device_log ...without captured columns) — verify the RPC returns unset optionals and the frontend falls back to UUIDUpsertCommandOnDeviceLog)🤖 Generated with Claude Code