Skip to content

Agent Activity: group by host in the grid, and expand in it - #1715

Merged
mastacontrola merged 1 commit into
feat/agent-enrollfrom
fix/agentactivity-rowgroup
Sep 5, 2026
Merged

Agent Activity: group by host in the grid, and expand in it#1715
mastacontrola merged 1 commit into
feat/agent-enrollfrom
fix/agentactivity-rowgroup

Conversation

@mastacontrola

Copy link
Copy Markdown
Member

The expand never worked

Clicking a host's chevron on this page rendered DataTables Responsive's hidden-column
list. The nested table it was meant to open was never constructed. Measured on a live
install at 1920px with zero columns hidden, so it was not a narrow-viewport artifact:

childTableInDom: false
childRowText:    "Events 134 / Last Activity 2026-09-04 19:08:39 / Last Event agent.usersession"
hiddenColsBeforeExpand: 0   ->   hiddenColsAfterExpand: 3

A DataTables row has one child slot. registerTable() turns Responsive on for every
grid and Responsive claims that slot, so row.child() handed it a table it then
overwrote. Nothing threw — which is why 7b207c2c9 could fix "the expanded host was
stuck at ten rows" by repairing a pager inside a table that did not exist.

What it is now

One grid, grouped by host with rowGroup. The header carries the host, its event count
and the expand control; expanding adds that host's events to the same grid as
ordinary rows. No table nested in a table, no second scrollbar, no second pager. The page
opens with every host visible and every group collapsed.

Three things had to be true for that to hold, each found by measuring rather than
reasoning:

Finding Consequence
A group whose rows are all filtered out renders no header Each host's newest event is seeded and never filtered — it anchors the header and is the row worth seeing when all is collapsed
rowGroup starts a new group whenever dataSrc changes down the ordered rows Ordering by time alone let one host's older events fall past the next host's newest and drew its header twice; a hidden column sorts every row of a host on that host's last-activity time plus its id
listem()'s recordsTotal is every row in auditLog (1435 on the lab install), not the host's The cap notice reads recordsFiltered; against recordsTotal it told a 134-event host it was truncated at 500

The flat event set stays unbounded — FOG_AUDIT_RETENTION_DAYS defaults to 0, keep
forever — which is why the seed is a summary query, each expansion is capped, and
rowGroup over a serverSide grid was never an option (it groups within one page).

Select and delete, on every page

Not specific to this page. A grid that passes select: false no longer gets Select All
and Deselect All, and a page that declares itself unselectable no longer gets "Delete
selected".

That was decided by a hardcoded list of node names — ['plugin', 'task', 'activity', 'audit'] — which this page was never added to, so it shipped a red Delete selected
over a table with no delete route anywhere in FOG (ADR 0021 Decision 8).
FOGPage::$selectable replaces the list and registerTable() drops the two buttons, so
each half is stated in the layer that enforces it. The 33 tables already passing
select: false stop showing two enabled buttons that did nothing.

Tests

tests/agent-activity-grouping.test.php — 21 checks. Each of its eight gates was
proven by reintroducing the defect and watching it go red
, then restored. It strips
comment lines before scanning: the first run failed on row.child() and on the old node
list, both of which appear only inside the comments that say never to go back to them.

tests/agent-activity-page.test.php had pinned the arrangement this replaces ("the grid
does not use rowGroup", the child table going through registerTable). Those three
checks now assert the opposite, and one of them that no child table returns by any route.

Both PHPStan passes clean, unscoped. Full suite 335 passed, 1 failed —
certificate-table.test.php, which fails identically on an unmodified checkout of the
base and is green in CI.

FOG_BCACHE_VER 362 → 363. No route changed, so OpenAPI::document() is untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JWJMQYE2br8E7Ehr55SJp2

The expand on this page never worked. Clicking a host's chevron rendered
DataTables Responsive's hidden-column list; the nested table it was meant
to open was never constructed. Measured on a live install at 1920px with
zero columns hidden, so it was not a narrow-viewport artifact.

A DataTables row has ONE child slot. registerTable() turns Responsive on
for every grid and Responsive claims that slot, so row.child() handed it a
table it then overwrote. Nothing threw -- which is why 7b207c2 could fix
"the expanded host was stuck at ten rows" by repairing a pager inside a
table that did not exist.

So there is no child row now. The page is one grid, grouped by host with
rowGroup: the header carries the host, its event count and the expand
control, and expanding adds that host's events to the same grid as ordinary
rows. No table nested in a table, no second scrollbar, no second pager.

Three things had to be true for that to hold, and each was found by
measuring rather than by reasoning:

- A group whose rows are all filtered out renders NO header, so collapsing
  by filtering would make every collapsed host vanish. Each host's newest
  event is therefore seeded into the table and never filtered. It anchors
  the header and doubles as the thing worth seeing when all is collapsed:
  what each agent last did.
- rowGroup starts a new group every time its dataSrc changes down the
  ORDERED rows. Ordering by time alone let one host's older events fall
  past the next host's newest and drew its header twice, so a hidden column
  sorts every row of a host on that host's last-activity time plus its id
  -- groups stay whole and stay ordered by recency rather than by name.
- listem()'s recordsTotal is every row in auditLog, not the host's. The cap
  notice now reads recordsFiltered; against recordsTotal it told a
  134-event host it was truncated at 500.

The flat event set stays unbounded (FOG_AUDIT_RETENTION_DAYS defaults to 0,
keep forever), which is why the seed is a summary query and each expansion
is capped, and why rowGroup over a serverSide grid was never an option.

Separately, and not specific to this page: a grid that says `select: false`
no longer gets Select All and Deselect All, and a page that says it is not
selectable no longer gets "Delete selected". That was decided by a
hardcoded list of node names, which this page was never added to -- so it
shipped a red Delete selected over a table with no delete route anywhere in
FOG (ADR 0021 Decision 8). FOGPage::$selectable replaces the list, and
registerTable() drops the two buttons, so both halves are stated where they
are enforced. The 33 tables already passing `select: false` stop showing
two enabled buttons that did nothing.

tests/agent-activity-grouping.test.php covers all of it, and each of its
eight gates was proven by reintroducing the defect and watching it go red.
It strips comment lines before scanning, because the first run failed on
row.child() and on the old node list where both appear only inside the
comments saying never to go back to them.

tests/agent-activity-page.test.php had pinned the arrangement this replaces
-- "the grid does not use rowGroup" and the child table going through
registerTable. Those three checks now assert the opposite, and one of them
that no child table comes back by any route.

FOG_BCACHE_VER 362 -> 363.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JWJMQYE2br8E7Ehr55SJp2
@mastacontrola
mastacontrola merged commit 9977339 into feat/agent-enroll Sep 5, 2026
11 checks passed
@mastacontrola
mastacontrola deleted the fix/agentactivity-rowgroup branch September 5, 2026 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant