diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index f70bb8923..0301b2b97 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "1d790655efb7b0c07c4a10f41a3814aa2313d2da5a5b387f40fbf1a2aae1e8c5", + "source_hash": "eb680f19bec2eebdbbc08c03606a9d42235e1f76d8d69a33f438dfd5ede4773c", "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 e78258e86..228e5b0b1 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -1071,7 +1071,37 @@ header .logout-btn:hover, line-height: 1.55; color: var(--ink-soft); } -.portal-support-card-grid { +.portal-support-brief-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; +} +.portal-support-brief-card { + display: flex; + flex-direction: column; + gap: 5px; + padding: 12px 14px; + border: 1px solid var(--line); + border-radius: 14px; + background: + linear-gradient( + 180deg, + #fdfefe 0%, + #f5f9fa 100%); +} +.portal-support-brief-card strong { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent-strong); +} +.portal-support-brief-card span { + font-size: 12px; + line-height: 1.5; + color: var(--ink-soft); +} +.portal-support-route-grid { display: grid; grid-template-columns: 1fr; gap: 14px; @@ -1082,21 +1112,25 @@ header .logout-btn:hover, gap: 14px; margin-top: 18px; } -.portal-support-card { +.portal-support-route-card { display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid var(--line); border-radius: var(--radius-md); - background: #fbfcfd; + background: + linear-gradient( + 180deg, + #fcfdfd 0%, + #f7fafb 100%); } -.portal-support-card h3 { +.portal-support-route-card h3 { margin: 0; font-size: 18px; letter-spacing: -0.02em; } -.portal-support-card p { +.portal-support-route-card p { margin: 0; font-size: 14px; line-height: 1.65; @@ -1122,6 +1156,25 @@ header .logout-btn:hover, line-height: 1.55; color: var(--ink-soft); } +.portal-support-handoff-note { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 0 0 12px; + border-top: 1px solid var(--line); + border-left: 2px solid rgba(15, 118, 110, 0.18); +} +.portal-support-handoff-note strong { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-muted); +} +.portal-support-handoff-note span { + font-size: 12px; + line-height: 1.5; + color: var(--ink-soft); +} .portal-support-runbook-list, .portal-support-points { display: flex; @@ -2518,7 +2571,8 @@ header .logout-btn:hover, padding: 18px 14px 40px; } .portal-shell-layout, - .portal-support-card-grid, + .portal-support-brief-strip, + .portal-support-route-grid, .portal-shell-nav-group { grid-template-columns: 1fr; } diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index fac13e5d4..bbb8051b0 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1942,7 +1942,7 @@ ]) + '

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

Keep access disciplined

Use the roster as a controlled operator surface, not a dumping ground for vague shared access.

Owners stay rareReserve Owner for billing, team, and full hosted control. Default to Admin, Tech, or Read-only first.
Keep operators narrowUse Tech for workspace operations and Read-only for verification instead of handing out broader access.
Remove stale access fastIf someone no longer owns the work, remove them instead of leaving dormant access attached to the account.

Invite and role policy

Keep the roster deliberate. Invite the smallest role first, then tighten access as responsibilities become clearer.

Invite someone new

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

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

Support desk

Use this desk when hosted access looks wrong, billing behaves unexpectedly, or you need help with commercial requests.

' + renderSectionContextChips(["Hosted issues", "Commercial requests", context.bootstrap.support_email ? "Email" : "Support"]) + '

Account support

Use this route when tenant handoff, workspace access, team control, or hosted billing looks wrong.

Route here for hosted issuesAccess, handoff, team, and hosted billing all belong on the hosted account path.
Keep the account context intactInclude the account, workspace, and action that failed so support can pick up the same operator path quickly.

Commercial services

Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the account services desk first.

Start in Account servicesUse the billing, license, refund, or privacy desk before escalating a commercial issue.
Escalate from the same deskKeep the request in one place instead of splitting it between billing and operator surfaces.

Route the issue cleanly

Keep hosted operations, commercial requests, and pure support escalation on their own paths so the next person does not have to reconstruct the account state.

1. Confirm the scopeDecide whether the problem is hosted operations, commercial self-service, or direct support escalation.
2. Keep hosted and commercial separateWorkspace and team problems stay in their own desks. Billing, license, refund, and privacy work stay in Account services.
3. Escalate with contextInclude the account, workspace, and exact failed action so the escalation path starts with the same facts you saw.
'; + return '
Support

Support desk

Use this desk when hosted access looks wrong, billing behaves unexpectedly, or you need help with commercial requests.

' + renderSectionContextChips(["Hosted issues", "Commercial requests", context.bootstrap.support_email ? "Email" : "Support"]) + '
Hosted pathWorkspace access, team control, tenant handoff, and hosted billing stay on the hosted account route.
Commercial pathSelf-hosted billing, licenses, refunds, and privacy requests stay in Account services until escalation is needed.
Escalate with contextInclude the exact account, workspace, desk, and failed action so support can continue the same path immediately.

Account support

Use this route when tenant handoff, workspace access, team control, or hosted billing looks wrong.

Route here for hosted issuesAccess, handoff, team, and hosted billing all belong on the hosted account path.
Keep the account context intactInclude the account, workspace, and action that failed so support can pick up the same operator path quickly.

Commercial services

Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the account services desk first.

Start in Account servicesUse the billing, license, refund, or privacy desk before escalating a commercial issue.
Escalate from the same deskKeep the request in one place instead of splitting it between billing and operator surfaces.

Route the issue cleanly

Keep hosted operations, commercial requests, and pure support escalation on their own paths so the next person does not have to reconstruct the account state.

1. Confirm the scopeDecide whether the problem is hosted operations, commercial self-service, or direct support escalation.
2. Keep hosted and commercial separateWorkspace and team problems stay in their own desks. Billing, license, refund, and privacy work stay in Account services.
3. Escalate with contextInclude the account, workspace, and exact failed action so the escalation path starts with the same facts you saw.
Include in the escalationAccount name, workspace name if relevant, the desk you were in, the exact button or request that failed, and whether the issue was hosted or commercial.
'; } function renderHeaderHTML(context) { if (context.bootstrap.authenticated) { diff --git a/internal/cloudcp/portal/frontend/src/shell_view.test.ts b/internal/cloudcp/portal/frontend/src/shell_view.test.ts index aacb02289..daf8bbf96 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -215,6 +215,9 @@ describe('shell view', function() { expect(html).toContain('Refund desk'); expect(html).toContain('Privacy desk'); expect(html).toContain('Route the issue cleanly'); + expect(html).toContain('Hosted path'); + expect(html).toContain('Commercial path'); + expect(html).toContain('Include in the escalation'); expect(html).toContain('Open account services'); }); diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 6c90b1c83..6b3fa3937 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -840,9 +840,23 @@ function renderSupportSection(context: ShellViewContext): string { '

Support desk

' + '

Use this desk when hosted access looks wrong, billing behaves unexpectedly, or you need help with commercial requests.

' + renderSectionContextChips(['Hosted issues', 'Commercial requests', context.bootstrap.support_email ? 'Email' : 'Support']) + + '
' + + '
' + + 'Hosted path' + + 'Workspace access, team control, tenant handoff, and hosted billing stay on the hosted account route.' + + '
' + + '
' + + 'Commercial path' + + 'Self-hosted billing, licenses, refunds, and privacy requests stay in Account services until escalation is needed.' + + '
' + + '
' + + 'Escalate with context' + + 'Include the exact account, workspace, desk, and failed action so support can continue the same path immediately.' + + '
' + + '
' + '
' + - '
' + - '
' + + '
' + + '
' + '' + '

Account support

' + '

Use this route when tenant handoff, workspace access, team control, or hosted billing looks wrong.

' + @@ -854,7 +868,7 @@ function renderSupportSection(context: ShellViewContext): string { '' + escapeHTML(context.bootstrap.support_email || '') + '' + '
' + '
' + - '
' + + '
' + '' + '

Commercial services

' + '

Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the account services desk first.

' + @@ -876,6 +890,10 @@ function renderSupportSection(context: ShellViewContext): string { '
2. Keep hosted and commercial separateWorkspace and team problems stay in their own desks. Billing, license, refund, and privacy work stay in Account services.
' + '
3. Escalate with contextInclude the account, workspace, and exact failed action so the escalation path starts with the same facts you saw.
' + '
' + + '
' + + 'Include in the escalation' + + 'Account name, workspace name if relevant, the desk you were in, the exact button or request that failed, and whether the issue was hosted or commercial.' + + '
' + '
' + '
' + '' diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index e3e1188a0..3f2b48578 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -1122,7 +1122,37 @@ header .logout-btn:hover, color: var(--ink-soft); } -.portal-support-card-grid { +.portal-support-brief-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; +} + +.portal-support-brief-card { + display: flex; + flex-direction: column; + gap: 5px; + padding: 12px 14px; + border: 1px solid var(--line); + border-radius: 14px; + background: linear-gradient(180deg, #fdfefe 0%, #f5f9fa 100%); +} + +.portal-support-brief-card strong { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent-strong); +} + +.portal-support-brief-card span { + font-size: 12px; + line-height: 1.5; + color: var(--ink-soft); +} + +.portal-support-route-grid { display: grid; grid-template-columns: 1fr; gap: 14px; @@ -1135,23 +1165,23 @@ header .logout-btn:hover, margin-top: 18px; } -.portal-support-card { +.portal-support-route-card { display: flex; flex-direction: column; gap: 10px; padding: 16px; border: 1px solid var(--line); border-radius: var(--radius-md); - background: #fbfcfd; + background: linear-gradient(180deg, #fcfdfd 0%, #f7fafb 100%); } -.portal-support-card h3 { +.portal-support-route-card h3 { margin: 0; font-size: 18px; letter-spacing: -0.02em; } -.portal-support-card p { +.portal-support-route-card p { margin: 0; font-size: 14px; line-height: 1.65; @@ -1181,6 +1211,28 @@ header .logout-btn:hover, color: var(--ink-soft); } +.portal-support-handoff-note { + display: flex; + flex-direction: column; + gap: 4px; + padding: 12px 0 0 12px; + border-top: 1px solid var(--line); + border-left: 2px solid rgba(15, 118, 110, 0.18); +} + +.portal-support-handoff-note strong { + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.portal-support-handoff-note span { + font-size: 12px; + line-height: 1.5; + color: var(--ink-soft); +} + .portal-support-runbook-list, .portal-support-points { display: flex; @@ -2787,7 +2839,8 @@ header .logout-btn:hover, } .portal-shell-layout, - .portal-support-card-grid, + .portal-support-brief-strip, + .portal-support-route-grid, .portal-shell-nav-group { grid-template-columns: 1fr; }