Skip to content

Info card: one-click Deploy/Capture, and no more "Primary Group" - #1712

Merged
mastacontrola merged 4 commits into
feat/agent-enrollfrom
feat/quick-tasking
Sep 5, 2026
Merged

Info card: one-click Deploy/Capture, and no more "Primary Group"#1712
mastacontrola merged 4 commits into
feat/agent-enrollfrom
feat/quick-tasking

Conversation

@mastacontrola

@mastacontrola mastacontrola commented Sep 5, 2026

Copy link
Copy Markdown
Member

Two changes to the same strip at the top of the host and group edit pages.

Based on feat/agent-enroll, and targeted at it, because fe5c6e73e rewrote the exact $this->notes block this touches (the Last Check-In agent/client logic). Doing it on working-1.6 would have guaranteed a hand-resolved conflict there.

Removed: the host card's "Primary Group"

It meant minId($this->obj->get('groups')) — the lowest-id group the host happened to be in.

There is no primary group. A group grants, and what a host ends up with is resolved from every group it belongs to at task time, ordered by groupOrder (ADR 0038, FOG\Assign\Resolver). So the label named a rank that does not exist and the value picked one membership arbitrarily.

Nothing replaces it. Listing every group instead was considered and dropped: a host in eight groups blows the card out, which makes the card worse rather than better, and the Group Associations tab already shows them properly.

Added: one-click tasking in the card

Deploy and Capture on the host card; Deploy and Multi-Cast on the group card. From whichever tab is open, so an admin already looking at a host does not have to go back to the grid and find the row again to do the obvious thing to it.

This is not a new pattern. The host list has carried the same buttons since _quickTaskItems(), for the reason that method documents: these are the task types that need no options, which is the whole reason they can be one click. The pairing is its own — Deploy and Capture are what a single host wants, Deploy and Multi-Cast what a set of them wants. On a group it is also what the server will accept: GroupManagement::deployPost() throws "Groups cannot create capture tasks" outright.

Every button confirms first, and this is the one place it deliberately differs from the list. There you tick a row to get the buttons; here you arrive on the page just by clicking a host name, so one stray click would deploy over a running machine — or, from a group, over all of them. The text is built server side because it is translated, and it names the target: the host, or the group and its member count, which is the fact that decides whether you meant to press it and the one thing the button itself cannot show.

Mechanically

  • FOGPage::$noteActions, a pre-rendered string, echoed by renderInfoCard() in a right-aligned column. ms-auto, not a float — the row is display:flex and a float would do nothing there.
  • It rides the existing EDIT_INFO_DATA hook alongside notes/noteSources, so a plugin adding a button does it the same way it already adds a line.
  • FOGPageRender::renderQuickTaskActions() builds them, gated on {node}.task — the action ?node=X&sub=deploy resolves to through Authorization::_subToAction(), so the gate here and the gate the POST hits are the same string by construction.
  • The script posts straight to ?node={node}&sub=deploy with scheduleType=instant, skipping the options form. Everything its POST is checked for — pending host, assigned and enabled image, protected image on a capture, one image across a multicast — is checked in deployPost(), not in the form. scheduleType is the one field that must be sent: validateScheduleType() throws on an absent value rather than defaulting.
  • Suppressed where the server would refuse anyway: a pending host, an empty group.

Button color is a contrast decision, not a taste one

btn-outline-secondary keeps #6c757d as the text color; against the dark card (#212529) that is 3.29:1, under the 4.5:1 AA floor for body-sized text. Filled btn-secondary puts white on #6c757d and holds 4.69:1 in both themes. Measured in a browser against the shipped adminlte4.min.css + fog-default-ui.min.css, rendering the real renderInfoCard() output. It is also what the list's own quick buttons are, so the two places offering the same tasking now look the same.

Neither is btn-primary: nothing here is the card's commit action — the General tab's Update is.

Verification

tests/info-card-quick-tasks.test.php (19 checks) pins the things that fail silently: the permission gate per node (host.task must not unlock the group card), the confirmation being present and naming the target, the values being read through get() rather than as properties, the pair each page asks for, and the filled class. Each was verified by reintroducing the defect and watching it go red.

  • phpstan pass 1: clean.
  • phpstan: both passes clean.
  • fogproject / upgrade rehearsal: green, reproduced locally against a throwaway mariadb:11.8 container and re-run on a second clean database.

It also fixes the two failures it inherited

feat/agent-enroll was red on phpstan and upgrade rehearsal before this branch existed, so this one inherited both. Neither was a code defect; both were a baseline the change that moved it did not update. Fixed here in 7c0c45199, so they clear on the base too when this merges.

  • phpstan pass 1AgentActivityManagement's constructor takes $name and ignores it. That is the shape every page in src/Pages has, and all 27 others are already in phpstan-baseline.neon under constructor.unusedParameter; the new page just had no entry. Added one. Changing the constructor instead would leave this page the only one in the directory with a different signature.
  • phpstan pass 2 — 26 entries for the agent test files. CI never reported these: the job runs the passes as separate steps with no continue-on-error, so pass 1 exiting 1 meant pass 2 never ran. They are the usual test shapes (PHPStan narrows a literal, so an in_array() pinning a constant reads as always-true), and phpstan-tests.neon's own header says this pass "adapts to the tests as they are written, it does not ask them to change" — so baselined, not rewritten. Generated into a temp file and merged: a straight regeneration emits only the 26 unignored errors and would have dropped the 196 already there, and it writes absolute paths that match nothing on a CI checkout. Verified as a set — 196 in, 222 out, 0 lost, no deletions in the diff.
  • upgrade rehearsal — three constraints landed that the fixture did not know about: fk_agentEnrollment_aeHostID, fk_agentWake_awSenderID, fk_agentWake_awTargetID, all agent tables referencing hosts. Declared 96 → 99, present 94 → 97; MISSING stays at the same 2 named constraints and the integrity block is untouched. The test's own docblock asks for exactly this update in the same commit.
  • tests/run-all.sh: 332 pass, 1 fail — certificate-table.test.php, which fails identically on an untouched feat/agent-enroll and on working-1.6, and passes in CI. Local environment, not this change.
  • Rendered through the real renderInfoCard() and screenshotted in both themes against the live stylesheets.

FOG_BCACHE_VER 360 → 361 for the changed script. It also covers the Agent Activity JS that arrived in the merge, which did not bump it. working-1.6 still sits at 360, so re-check the value against the base when feat/agent-enroll merges up — two branches bumping 360 → 361 merge cleanly and silently collide.

No REST route changed, so OpenAPI::document() is unaffected. No downstream class-list change, so nothing for FogApi.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JWJMQYE2br8E7Ehr55SJp2

mastacontrola and others added 4 commits September 5, 2026 07:43
Two changes to the same strip at the top of the host and group edit pages.

REMOVED: the host card's "Primary Group". It meant
minId($this->obj->get('groups')) -- the lowest-id group the host happened
to be in. There is no primary group. A group GRANTS, and what a host ends
up with is resolved from every group it belongs to at task time, ordered
by groupOrder (ADR 0038, FOG\Assign\Resolver). So the label named a rank
that does not exist and the value picked one membership arbitrarily.

Nothing replaces it. Listing every group instead was considered and
dropped: a host in eight groups blows the card out, which makes the card
worse rather than better, and the Group Associations tab already shows
them properly.

ADDED: Deploy and Capture on the host card, Deploy and Multi-Cast on the
group card. One click, from whichever tab is open, so an admin already
looking at a host does not have to go back to the grid and find the row
again to do the obvious thing to it.

This is not a new pattern. The host LIST has carried the same three
buttons since _quickTaskItems(), for the same reason: these are the task
types that need no options, which is the whole reason they can be one
click. The pairing is the one that method already documents -- Deploy and
Capture are what a single host wants, Deploy and Multi-Cast what a set of
them wants. On a group it is also what the server will accept:
GroupManagement::deployPost() throws "Groups cannot create capture tasks"
outright, so a Capture button there could only produce a toast saying no.

Every button confirms first, and this is the one place it deliberately
differs from the list. There you tick a row to get the buttons; here you
arrive on the page just by clicking a host name, so one stray click would
deploy over a running machine -- or, from a group, over all of them. The
text is built server side because it is translated, and it names the
target: the host, or the group AND its member count, which is the fact
that decides whether you meant to press it and the one thing the button
itself cannot show.

Mechanically:

- FOGPage::$noteActions, a pre-rendered string, echoed by
  renderInfoCard() in a right-aligned column. ms-auto, not a float: the
  row is display:flex and a float would do nothing there.
- It rides the existing EDIT_INFO_DATA hook alongside notes/noteSources,
  so a plugin adding a button does it the same way it already adds a
  line. A second event would mean two registrations for one card.
- FOGPageRender::renderQuickTaskActions() builds them, gated on
  {node}.task -- the action ?node=X&sub=deploy resolves to through
  Authorization::_subToAction(), so the gate here and the gate the POST
  hits are the same string by construction.
- The script posts straight to ?node={node}&sub=deploy with
  scheduleType=instant, skipping the options form. Nothing on it these
  types need, and everything its POST is checked for -- pending host,
  assigned and enabled image, protected image on a capture, one image
  across a multicast -- is checked in deployPost(), not in the form.
  scheduleType is the one field that must be sent: validateScheduleType()
  throws on an absent value rather than defaulting.
- Suppressed where the server would refuse anyway: a pending host, an
  empty group.

Neutral outline buttons rather than a type color. Nothing here is the
card's commit action -- the General tab's Update is -- and these are
shortcuts in a header strip, not a decision cluster in a form footer. The
weight a red button would carry is carried by the confirmation instead.

tests/info-card-quick-tasks.test.php pins the four things that fail
silently: the permission gate per node (host.task must not unlock the
group card), the confirmation being present AND naming the target, the
values being read through get() rather than as properties, and the pair
each page asks for. Each was verified by reintroducing the defect and
watching it go red.

FOG_BCACHE_VER 360 -> 361 for the changed script.

Co-Authored-By: Claude <noreply@anthropic.com>
btn-outline-secondary keeps #6c757d as the TEXT color. Against the dark
card (#212529) that is 3.29:1 -- under the 4.5:1 AA floor for body-sized
text. Filled puts white on #6c757d instead and holds 4.69:1 in both
themes.

Measured in a browser against the shipped adminlte4.min.css and
fog-default-ui.min.css, rendering the real renderInfoCard() output, not
assumed from the Bootstrap defaults.

It is also what the host list's own quick buttons already are, since
DataTables draws its button bar filled -- so the two places that offer
the same one-click tasking now look the same.

The test pins the class rather than the ratio: a browser is what measures
contrast and the suite is PHP, but the class is the part a future edit
would actually change.

Co-Authored-By: Claude <noreply@anthropic.com>
Both checks were red on #1707 before this branch existed, and #1712
inherited them. Neither is a code defect -- both are a baseline that the
change which moved it did not update.

phpstan pass 1: AgentActivityManagement's constructor takes $name and
ignores it, overwriting with _('Agent Activity'). That is the shape EVERY
page in src/Pages has -- TaskManagement, HostManagement, GroupManagement,
ReportManagement, all of them -- and every one of the others is already in
phpstan-baseline.neon under constructor.unusedParameter. The new page just
had no entry. Added one, in path order beside its neighbours. Changing the
constructor instead would make this page the only one in the directory
with a different signature, to satisfy a rule the other 27 are exempt from.

phpstan pass 2: 26 entries for the agent test files. Worth saying that CI
never reported these -- the job runs both passes as separate steps without
continue-on-error, so pass 1 exiting 1 meant pass 2 never ran. Fixing pass
1 is what makes them visible, and they would have turned the check red
again on the next push looking like a new regression.

They are the usual test-file shapes: PHPStan narrows a literal, so an
in_array() that pins a constant against its own list, or a ?? guarding an
offset it can prove exists, reads as "always true". phpstan-tests.neon's
own header says this pass "adapts to the tests as they are written, it does
not ask them to change", so they are baselined, not rewritten.

Generated with --generate-baseline into a temp file and MERGED, not
applied: a straight regeneration emits only the 26 unignored errors and
would have dropped the 196 entries already there. Paths rewritten
repo-relative -- the generator writes absolute ones, which would match
nothing on a CI checkout. Verified as a set: 196 in, 222 out, 0 lost, 26
added, no deletions in the diff.

upgrade rehearsal: 3 constraints landed that the fixture did not know
about -- fk_agentEnrollment_aeHostID, fk_agentWake_awSenderID and
fk_agentWake_awTargetID, all agent tables referencing hosts. So declared
96 -> 99 and present 94 -> 97, while MISSING stays at the same 2 named
constraints and the integrity block is untouched. Nothing regressed; the
count moved because the schema grew.

The test's own docblock asks for exactly this: "a change that legitimately
moves the numbers -- adding a constraint -- updates it in the SAME commit
and the diff is visible in review."

Reproduced locally first against a throwaway mariadb:11.8 container rather
than the lab server, got CI's diff byte for byte, confirmed the three new
constraints in information_schema, then updated the fixture and re-ran on
a second clean database to check it passes from scratch rather than only
on the one it was generated from.

Co-Authored-By: Claude <noreply@anthropic.com>
@mastacontrola
mastacontrola merged commit 5f86499 into feat/agent-enroll Sep 5, 2026
10 checks passed
@mastacontrola
mastacontrola deleted the feat/quick-tasking branch September 5, 2026 13:54
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