diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index 83a3ebbc4e..8de6137af1 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "8b0b284da60918f341d35eb2e51aed2893c7af4b399d949fd6dc0cef3e166408", + "source_hash": "06fe7351022ab56b379fb5059391505df0cd8b1ddb0c897f4b7ba30a9cca6866", "build_inputs": [ "package.json", "tsconfig.json", diff --git a/internal/cloudcp/portal/dist/portal_app.css b/internal/cloudcp/portal/dist/portal_app.css index 0e2445474b..5b36fb287b 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -1101,11 +1101,22 @@ header .logout-btn:hover, grid-template-columns: minmax(0, 1.65fr) minmax(320px, 0.92fr); gap: 14px; } +.team-side-column { + display: flex; + flex-direction: column; + gap: 14px; +} .team-roster, .team-invite-panel { background: #fff; border-color: var(--line); } +.team-policy-panel { + padding: 16px; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; +} .team-panel-heading { margin-bottom: 12px; } @@ -1232,27 +1243,32 @@ header .logout-btn:hover, flex-direction: column; gap: 12px; } -.team-invite-guide { +.team-policy-list { display: flex; flex-direction: column; - gap: 6px; - padding: 10px 12px; + gap: 0; border: 1px solid var(--line); border-radius: 10px; - background: #fff; + background: var(--panel-muted); + overflow: hidden; } -.team-invite-guide-row { +.team-policy-row { display: flex; flex-direction: column; - gap: 2px; + gap: 4px; + padding: 12px 14px; + border-top: 1px solid var(--line); } -.team-invite-guide-row strong { +.team-policy-row:first-child { + border-top: none; +} +.team-policy-row strong { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); } -.team-invite-guide-row span { +.team-policy-row span { font-size: 12px; line-height: 1.4; color: var(--ink-soft); diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index bf2fbf5574..130ae76cdd 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1813,7 +1813,8 @@ return '

Hosted fleet

Use the fleet view to open workspaces, watch health posture, and keep management actions explicit.

' + workspaceHTML + '
' + workspaceManagement + "
"; } function renderAccountTeamSection(account) { - return '

Control who can operate this account

Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.

People on this account

Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.

Loading\u2026

Invite someone new

Add another operator with the minimum role they need for this account.

AdminBilling and hosted operations.
TechOperational access without billing control.
Read-onlyReview hosted state without making changes.
'; + var accessPolicy = '

Access model

Assign the smallest role that still lets someone do the work they own on this account.

OwnerBilling, team access, and full hosted control.
AdminHosted operations plus billing for the account.
TechWorkspace operations without billing ownership.
Read-onlyState review and verification without control-plane changes.
'; + return '

Control who can operate this account

Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.

People on this account

Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.

Loading\u2026

Invite someone new

Add another operator with the minimum role they need for this account.

' + accessPolicy + "
"; } function renderSupportSection(context) { return '
Support

Support and escalation

Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.

Account support

For access, tenant handoff, team, and billing issues, contact the hosted operations desk.

' + escapeHTML(context.bootstrap.support_email || "") + '

Commercial services

Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.

'; diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index b31d3cfa25..add766d257 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -169,6 +169,8 @@ describe('shell view', function() { expect(html).toContain('data-action="clear-workspace-selection"'); expect(html).toContain('Team management'); expect(html).toContain('Invite someone new'); + expect(html).toContain('Access model'); + expect(html).toContain('Workspace operations without billing ownership.'); expect(html).toContain('People on this account'); expect(html).toContain('data-action="workspace-action"'); expect(html).toContain('service-action-row'); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 9c451ece25..e1686f8dcb 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -579,6 +579,20 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI } function renderAccountTeamSection(account: PortalAccountSummary): string { + var accessPolicy = + '
' + + '
' + + '

Access model

' + + '

Assign the smallest role that still lets someone do the work they own on this account.

' + + '
' + + '
' + + '
OwnerBilling, team access, and full hosted control.
' + + '
AdminHosted operations plus billing for the account.
' + + '
TechWorkspace operations without billing ownership.
' + + '
Read-onlyState review and verification without control-plane changes.
' + + '
' + + '
'; + return ( '
' + '
Loading…' + '' + '' + - '
' + - '
' + - '

Invite someone new

' + - '

Add another operator with the minimum role they need for this account.

' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
AdminBilling and hosted operations.
' + - '
TechOperational access without billing control.
' + - '
Read-onlyReview hosted state without making changes.
' + + '
' + + '
' + + '
' + + '

Invite someone new

' + + '

Add another operator with the minimum role they need for this account.

' + + '
' + + '
' + + '
' + + '
' + + '' + '
' + - '' + '
' + + accessPolicy + '
' + '
' + '
' + diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index 8e6b05dade..3f1d81c201 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -1235,12 +1235,25 @@ header .logout-btn:hover, gap: 14px; } +.team-side-column { + display: flex; + flex-direction: column; + gap: 14px; +} + .team-roster, .team-invite-panel { background: #fff; border-color: var(--line); } +.team-policy-panel { + padding: 16px; + border: 1px solid var(--line); + border-radius: 12px; + background: #fff; +} + .team-panel-heading { margin-bottom: 12px; } @@ -1386,30 +1399,36 @@ header .logout-btn:hover, gap: 12px; } -.team-invite-guide { +.team-policy-list { display: flex; flex-direction: column; - gap: 6px; - padding: 10px 12px; + gap: 0; border: 1px solid var(--line); border-radius: 10px; - background: #fff; + background: var(--panel-muted); + overflow: hidden; } -.team-invite-guide-row { +.team-policy-row { display: flex; flex-direction: column; - gap: 2px; + gap: 4px; + padding: 12px 14px; + border-top: 1px solid var(--line); } -.team-invite-guide-row strong { +.team-policy-row:first-child { + border-top: none; +} + +.team-policy-row strong { font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-muted); } -.team-invite-guide-row span { +.team-policy-row span { font-size: 12px; line-height: 1.4; color: var(--ink-soft);