diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index be814186cc..529efb7fc4 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -925,6 +925,10 @@ responsive shell appear to ignore an operator. split: cloud-paid owns Suspend, Activate, Reload, tenant billing state, and mutation semantics, while frontend-primitives owns the shared `Button` chrome used by those row actions. + On phones, the tenant grid must retain organization identity, subscription + state, and mutation actions. Owner IDs, trial detail, and Stripe customer + IDs remain desktop review fields or expanded billing-state detail rather + than forcing the primary mobile decision surface into horizontal scanning. 14. Add or change shared commercial plan/usage presentation through `frontend-modern/src/components/Settings/CommercialBillingSections.tsx` and `frontend-modern/src/utils/commercialBillingModel.ts` 15. Add or change organization billing and usage presentation through `frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx`, and `frontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts` 16. Add or change self-hosted Pro plan, recovery, and entitlement actions through `frontend-modern/src/components/Settings/ProLicensePanel.tsx`, `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`, `frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx`, and `frontend-modern/src/components/Settings/useProLicensePanelState.ts` diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index a16d83fa04..0ab44309cc 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1072,9 +1072,14 @@ not a replacement status card, CTA band, or page-local nested card. focus, and embedded-row propagation behavior. Global app-shell prompts are part of the same action boundary. `frontend-modern/src/components/GitHubStarBanner.tsx` may own its display - timing, product copy, and GitHub destination, but its primary, defer, and - dismiss controls must compose `Button` and `ActionIconButton` instead of - carrying local floating-prompt button shells. + timing, product copy, and GitHub destination, but its primary and dismiss + controls must compose `Button` and `ActionIconButton` instead of carrying + local floating-prompt button shells. The prompt must wait for fourteen + distinct active days with connected infrastructure, consume at most one + lifetime appearance, treat close as permanent dismissal, and never restore + recurring snooze behavior. It must yield the browser session to an existing + release, disclosure, or other low-priority app-shell notice and suppress + itself for the remainder of a session that presents a blocking dialog. Settings selection helpers such as `ResourcePicker` must use the same `Button` primitive for select-all, clear, and chip remove actions instead of restoring footer-local action shells. @@ -4987,6 +4992,9 @@ update-surface wording inline. `CopyCommandBlock` must use the shared `copyToClipboard` helper so install/update/agent snippets keep the same Clipboard API fallback path and only report copied state after the shared copy path succeeds. +The running published version must keep a direct `Current release notes` link +in this settings shell so suppressing or dismissing the one-time post-update +notice never makes the changelog undiscoverable. The update verdict is honest about its age. The frontend serves the update verdict from a 24-hour localStorage cache, so the panel's "Up to date" state @@ -6091,6 +6099,15 @@ disclosure, navigation, copy, and row-action controls must keep a 40-pixel mobile touch floor while retaining their compact desktop density. Settings navigation must own a viewport-bounded vertical scroll region so its route list does not push the active panel below the page. +Settings data grids must also define a phone information hierarchy instead of +depending on horizontal scrolling as their first responsive behavior. Identity, +current state, and immediately available actions stay visible; lower-priority +timestamps, external IDs, and verbose detail columns may be hidden through the +shared symmetric column boundary. When a hidden field is still needed for the +phone decision, its concise value belongs as mobile-only secondary context in +the surviving identity cell. Multi-action cells may collapse visible labels on +phones only when every action keeps an accessible name and the shared touch +floor. Patrol finding handoffs must derive approval posture from the canonical typed action state and approval policy, not only from a legacy approval id. A @@ -6478,7 +6495,8 @@ element ref, because a ref runs before the node is in the document, where the measured height is zero. Every surface that must sit on top of the bar reads that property: the -Assistant overlay panel and its backdrop, the global GitHub star banner, and +Assistant overlay panel and its backdrop, the compact post-update notice, the +global GitHub star banner, and the `.filter-bottom-nav-aware-panel` rule in `frontend-modern/src/index.css`. Consumers must not add `env(safe-area-inset-bottom)` on top of the published value, and must not diff --git a/docs/release-control/v6/internal/subsystems/organization-settings.md b/docs/release-control/v6/internal/subsystems/organization-settings.md index 6a4c83a863..9bba34053c 100644 --- a/docs/release-control/v6/internal/subsystems/organization-settings.md +++ b/docs/release-control/v6/internal/subsystems/organization-settings.md @@ -80,6 +80,11 @@ create, review, and approve cross-organization shares. come from `frontend-primitives` through `PulseDataGrid`/`Table`; do not add organization-local `overflow-x-auto`, negative-margin, or side-border compensation around grids. + Their phone projection must keep the identity, current access/state, and + available row action needed for an immediate decision. Added/requested + timestamps, repeated permission detail, and other review metadata are + desktop detail columns; they must not squeeze those decision fields into an + unreadable six-column phone grid. Organization role and share-status badges follow the same primitive boundary: role/status semantics remain organization-owned, but visible badge shell, tone vocabulary, and label rendering must compose diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 26615cc2ea..7fec7237f9 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,35 +1,54 @@ { "version": 1, - "base_sha": "3678d583bf43840d0086e4ba70c81aec77dadd01", - "verified_at": "2026-08-25T19:06:05Z", + "base_sha": "b3d9984f7e3a92b9b8f13ecd71efcd8a7babc923", + "verified_at": "2026-08-25T19:34:45Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Workloads/guestDrawerModel.ts" + "frontend-modern/src/components/Settings/AuditLogPanel.tsx", + "frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx", + "frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx", + "frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx", + "frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx", + "frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx", + "frontend-modern/src/components/Settings/ReportingPanel.tsx", + "frontend-modern/src/components/Settings/RolesPanel.tsx", + "frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx" ], "content_sha256": { - "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "5aaccadfaa746f391431c5138e7a1c051b643e264e57fb8e2f58f121cffb0de9" + "frontend-modern/src/components/Settings/AuditLogPanel.tsx": "8dc3a831609e02c69bc497be2c91daea5ced4c59f79cbf554fa3fc41b05b1f65", + "frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx": "47f94e29a76f9f545029c7ae8f4ee7e7b66e5c5b32fb4458608d24e57bd70b50", + "frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx": "d2dbc0bbd350b7871a6591c146079972be520769987d91dd101c2aa840a552aa", + "frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx": "f22e4a8c7fc2898561d1039d1c9885e84c2246a788ef086e4868a74e93502b67", + "frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx": "a4764da645e44ade198a438091b092c89d2a90021dbea85004ed0e911997f42f", + "frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx": "8f0a16bdd5523e54520f66fab68fde9571d682ba088cc75e111b6949d4a5c471", + "frontend-modern/src/components/Settings/ReportingPanel.tsx": "004e78cc0b32263fa05555d43b6fbc3a61b8b0d6aa5dda4b3a16dbee01a42547", + "frontend-modern/src/components/Settings/RolesPanel.tsx": "293d21bf65340202581b314ae6c402b19a9f29dbcc4dbebc841c20989e205b53", + "frontend-modern/src/components/Settings/UserAssignmentsPanel.tsx": "4651822c6467732b182eb5a888beeee5b976f1819fab12cc357d69095659ce20" }, "routes": [ - "/proxmox/overview", - "/docker/overview", - "/kubernetes/overview", - "/truenas/apps" + "/settings/security-audit", + "/settings/support/reporting", + "/settings/security-roles", + "/settings/security-users", + "/settings/organization/access", + "/settings/organization/sharing", + "/settings/organization/billing-admin" ], "viewports": [ - { "width": 1280, "height": 800 }, + { "width": 1440, "height": 900 }, { "width": 390, "height": 844 } ], "states": [ - "stopped Proxmox guest History drawer using the negative unknown-disk sentinel", - "running Docker container History drawer with populated utilization data", - "degraded Kubernetes pod History drawer with populated utilization and network data", - "TrueNAS app Overview drawer with long container and identity values" + "audit log, reporting, roles, and user access paid-feature fallbacks in the local self-hosted session", + "organization access and billing-admin capability redirects", + "organization sharing unavailable-capability route state", + "populated mobile column priorities and row-action names exercised by the targeted component tests" ], "interactions": [ - "opened the stopped dev-portal Proxmox guest and switched from Overview to History at 390px", - "verified the unavailable disk fallback renders as a dash instead of a negative percentage at 390px and 1280px", - "opened Docker and Kubernetes History tabs to confirm valid current metrics and chart geometry remain intact", - "opened a TrueNAS app drawer and inspected long-value truncation, stacking, scrolling, and tab placement", + "navigated every affected settings route at 390px and 1440px", + "confirmed zero document-level horizontal overflow on every rendered route state", + "confirmed the phone projections retain primary identity, current state, and actions while desktop detail remains available", + "confirmed report schedule phone actions collapse behind one 44px accessible actions trigger", "restored the browser viewport after responsive verification" ] } diff --git a/frontend-modern/src/components/Settings/AuditLogPanel.tsx b/frontend-modern/src/components/Settings/AuditLogPanel.tsx index 00efb5b8e4..db0462dc7b 100644 --- a/frontend-modern/src/components/Settings/AuditLogPanel.tsx +++ b/frontend-modern/src/components/Settings/AuditLogPanel.tsx @@ -381,6 +381,7 @@ export default function AuditLogPanel() { label: 'Status', width: '64px', align: 'center', + hiddenOnMobile: true, render: (event) => { const presentation = getAuditEventStatusPresentation(event.success); const Icon = presentation.icon; @@ -390,6 +391,7 @@ export default function AuditLogPanel() { { key: 'timestamp', label: 'Timestamp', + hiddenOnMobile: true, render: (event) => ( {formatTimestamp(event.timestamp)} ), @@ -398,16 +400,23 @@ export default function AuditLogPanel() { key: 'event', label: 'Event', render: (event) => ( - - {event.event} - +
+ + {event.event} + +
+ {event.success ? 'Success' : 'Failed'} · {event.user || 'Unknown user'} ·{' '} + {formatTimestamp(event.timestamp)} +
+
), }, { key: 'user', label: 'User', + hiddenOnMobile: true, render: (event) => {event.user || '-'}, }, { diff --git a/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx b/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx index f2115d3ba8..f988628948 100644 --- a/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx +++ b/frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx @@ -67,6 +67,7 @@ export const BillingAdminOrganizationsTable: Component ( {org.owner_user_id || 'N/A'} ), @@ -86,6 +87,7 @@ export const BillingAdminOrganizationsTable: Component { const billing = props.billingByOrgID[(org.org_id || '').trim()]; return ( @@ -96,6 +98,7 @@ export const BillingAdminOrganizationsTable: Component { const billing = props.billingByOrgID[(org.org_id || '').trim()]; const cellValue = stripeCustomerCell(billing); diff --git a/frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx b/frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx index 15fc8adc11..8c69e1edba 100644 --- a/frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx +++ b/frontend-modern/src/components/Settings/OrganizationAccessMembersSection.tsx @@ -74,6 +74,7 @@ export const OrganizationAccessMembersSection: Component {formatOrgDate(member.addedAt)}, }, { diff --git a/frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx b/frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx index c230f61353..334131749b 100644 --- a/frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx +++ b/frontend-modern/src/components/Settings/OrganizationIncomingSharesSection.tsx @@ -49,6 +49,7 @@ export const OrganizationIncomingSharesSection: Component< { key: 'accessRole', label: 'Access', + hiddenOnMobile: true, render: (share) => { const role = normalizeOrganizationShareRole(share.accessRole); return ; @@ -73,6 +74,7 @@ export const OrganizationIncomingSharesSection: Component< { key: 'createdAt', label: 'Requested', + hiddenOnMobile: true, render: (share) => {formatOrgDate(share.createdAt)}, }, { diff --git a/frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx b/frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx index 8d39cdb7e7..d22bf93d09 100644 --- a/frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx +++ b/frontend-modern/src/components/Settings/OrganizationOutgoingSharesSection.tsx @@ -52,6 +52,7 @@ export const OrganizationOutgoingSharesSection: Component< { key: 'accessRole', label: 'Access', + hiddenOnMobile: true, render: (share) => { const role = normalizeOrganizationShareRole(share.accessRole); return ; @@ -76,6 +77,7 @@ export const OrganizationOutgoingSharesSection: Component< { key: 'createdAt', label: 'Requested', + hiddenOnMobile: true, render: (share) => {formatOrgDate(share.createdAt)}, }, { diff --git a/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx b/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx index 8533e92e02..d50a4b5014 100644 --- a/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx +++ b/frontend-modern/src/components/Settings/OrganizationOverviewMembersSection.tsx @@ -34,6 +34,7 @@ export const OrganizationOverviewMembersSection: Component< { key: 'addedAt', label: 'Added', + hiddenOnMobile: true, render: (member) => {formatOrgDate(member.addedAt)}, }, ]} diff --git a/frontend-modern/src/components/Settings/ReportingPanel.tsx b/frontend-modern/src/components/Settings/ReportingPanel.tsx index d3f9ef7537..89cd3841bf 100644 --- a/frontend-modern/src/components/Settings/ReportingPanel.tsx +++ b/frontend-modern/src/components/Settings/ReportingPanel.tsx @@ -4,6 +4,7 @@ import Download from 'lucide-solid/icons/download'; import BarChart from 'lucide-solid/icons/bar-chart'; import TableProperties from 'lucide-solid/icons/table-properties'; import Plus from 'lucide-solid/icons/plus'; +import EllipsisVertical from 'lucide-solid/icons/ellipsis-vertical'; import Pencil from 'lucide-solid/icons/pencil'; import Play from 'lucide-solid/icons/play'; import RefreshCw from 'lucide-solid/icons/refresh-cw'; @@ -353,8 +354,10 @@ export function ReportingPanel() { - - + + @@ -364,8 +367,10 @@ export function ReportingPanel() { - - + + @@ -377,9 +382,12 @@ export function ReportingPanel() { title={schedule.name} > {schedule.name} + + {reportScheduleCadenceLabel(schedule)} +
NameCadenceName Scope EnabledActionsEnabled + Actions +
{reportScheduleCadenceLabel(schedule)} @@ -418,12 +426,59 @@ export function ReportingPanel() { -
+
+ + + +
+ + + +
+
+ ), diff --git a/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx index 0e96c627a0..8dd5831f94 100644 --- a/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx @@ -151,6 +151,14 @@ describe('BillingAdminPanel', () => { expect(billingAdminOrganizationsTableSource).toContain('variant="secondary"'); expect(billingAdminOrganizationsTableSource).toContain('size="sm"'); expect(billingAdminOrganizationsTableSource).toContain('size="xs"'); + for (const key of ['owner', 'trial', 'stripeCustomer']) { + expect(billingAdminOrganizationsTableSource).toMatch( + new RegExp(`key: '${key}',[\\s\\S]*?hiddenOnMobile: true`), + ); + } + expect(billingAdminOrganizationsTableSource).not.toMatch( + /key: 'subscription',\s+label: 'Subscription',\s+hiddenOnMobile:/, + ); expect(billingAdminOrganizationsTableSource).not.toContain( 'px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50', ); diff --git a/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx b/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx index 9c40f6589a..0d4f708dc4 100644 --- a/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx @@ -240,7 +240,7 @@ describe('RBAC paywall settings panels', () => { expect(getUsersMock).toHaveBeenCalled(); expect(getRolesMock).toHaveBeenCalled(); expect(screen.getByPlaceholderText('Search users...')).not.toBeDisabled(); - expect(screen.getByRole('button', { name: 'Manage Access' })).toHaveClass( + expect(screen.getByRole('button', { name: 'Manage access for alice' })).toHaveClass( 'min-h-11', 'sm:min-h-9', ); @@ -265,7 +265,7 @@ describe('RBAC paywall settings panels', () => { expect(screen.getByText('OIDC · okta')).toBeInTheDocument(); expect(screen.getByTitle('sso:oidc:okta:opaque_subject')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: 'Remove' })); + fireEvent.click(screen.getByRole('button', { name: 'Remove Alice Example' })); const dialog = await screen.findByRole('dialog', { name: 'Remove user access: Alice Example' }); expect(dialog).toHaveTextContent('does not disable the account at the identity provider'); @@ -331,7 +331,7 @@ describe('RBAC paywall settings panels', () => { expect(screen.getByText('alice')).toBeInTheDocument(); }); - fireEvent.click(screen.getByRole('button', { name: 'Manage Access' })); + fireEvent.click(screen.getByRole('button', { name: 'Manage access for alice' })); await waitFor(() => { expect(screen.getByRole('dialog', { name: 'Manage access: alice' })).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index d5833382be..a89f1d3bf8 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -83,6 +83,7 @@ import organizationAccessMembersSectionSource from '../OrganizationAccessMembers import organizationIncomingSharesSectionSource from '../OrganizationIncomingSharesSection.tsx?raw'; import organizationOutgoingSharesSectionSource from '../OrganizationOutgoingSharesSection.tsx?raw'; import organizationOverviewDetailsSectionSource from '../OrganizationOverviewDetailsSection.tsx?raw'; +import organizationOverviewMembersSectionSource from '../OrganizationOverviewMembersSection.tsx?raw'; import organizationSharingCreateSectionSource from '../OrganizationSharingCreateSection.tsx?raw'; import rolesEditorDialogSource from '../RolesEditorDialog.tsx?raw'; import rolesPanelSource from '../RolesPanel.tsx?raw'; @@ -720,6 +721,37 @@ describe('settings architecture guardrails', () => { ); }); + it('keeps dense settings tables on deliberate phone information priorities', () => { + const expectHiddenColumn = (source: string, key: string) => { + expect(source).toMatch( + new RegExp(`key: '${key}',[\\s\\S]*?label: '[^']+',[\\s\\S]*?hiddenOnMobile: true`), + ); + }; + + expectHiddenColumn(organizationAccessMembersSectionSource, 'addedAt'); + expectHiddenColumn(organizationOverviewMembersSectionSource, 'addedAt'); + expectHiddenColumn(organizationIncomingSharesSectionSource, 'accessRole'); + expectHiddenColumn(organizationIncomingSharesSectionSource, 'createdAt'); + expectHiddenColumn(organizationOutgoingSharesSectionSource, 'accessRole'); + expectHiddenColumn(organizationOutgoingSharesSectionSource, 'createdAt'); + expectHiddenColumn(rolesPanelSource, 'permissions'); + expectHiddenColumn(auditLogPanelSource, 'success'); + expectHiddenColumn(auditLogPanelSource, 'timestamp'); + expectHiddenColumn(auditLogPanelSource, 'user'); + + expect(auditLogPanelSource).toContain('text-[11px] text-muted sm:hidden'); + expect(auditLogPanelSource).toContain("event.user || 'Unknown user'"); + expect(userAssignmentsPanelSource).toContain( + '', + ); + expect(userAssignmentsPanelSource).toContain(''); + expect(reportingPanelSource).toContain('hidden w-[16%] px-3 py-2 font-semibold sm:table-cell'); + expect(reportingPanelSource).toContain('text-xs font-normal text-muted sm:hidden'); + expect(reportingPanelSource).toContain('
'); + expect(reportingPanelSource).toContain('aria-label={`Actions for ${schedule.name}`}'); + expect(reportingPanelSource).toContain('class="hidden justify-end gap-1 sm:flex"'); + }); + it('keeps settings loading skeletons on the shared skeleton primitive', () => { for (const source of [dataHandlingPanelSource, securityOverviewPanelSource]) { expect(source).toContain('@/components/shared/SettingsLoadingSkeleton'); diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index 9f3345a43f..076f5c0fa5 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -3030,7 +3030,7 @@ class SubsystemLookupTest(unittest.TestCase): { "heading": "## Extension Points", "path": "frontend-modern/src/api/orgs.ts", - "line": 89, + "line": 94, "heading_line": 74, }, ], @@ -3105,7 +3105,7 @@ class SubsystemLookupTest(unittest.TestCase): { "heading": "## Extension Points", "path": "internal/api/access_control_handlers.go", - "line": 103, + "line": 108, "heading_line": 74, }, ],