From 8fb55097ecf23a2beb4641bcefcfd5eb36ae0dd3 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 28 Mar 2026 04:40:06 +0000 Subject: [PATCH] style(portal): turn support into escalation desk --- .../cloudcp/portal/dist/build_manifest.json | 2 +- internal/cloudcp/portal/dist/portal_app.css | 74 +++++++++++++++- internal/cloudcp/portal/dist/portal_app.js | 2 +- .../portal/frontend/src/shell_view.test.ts | 2 + .../cloudcp/portal/frontend/src/shell_view.ts | 45 ++++++++-- .../cloudcp/portal/frontend/src/styles.css | 85 ++++++++++++++++++- 6 files changed, 195 insertions(+), 15 deletions(-) diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json index e48a7ea31..e22d72057 100644 --- a/internal/cloudcp/portal/dist/build_manifest.json +++ b/internal/cloudcp/portal/dist/build_manifest.json @@ -1,5 +1,5 @@ { - "source_hash": "5b47e0e6bc3d415732986d172946239c90f4770278b376eac60332e2e43e6e23", + "source_hash": "feb5de02547ef564c065366222a3551f4d56198f98db27747b883bd79f938116", "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 9d85a2289..710a9c37c 100644 --- a/internal/cloudcp/portal/dist/portal_app.css +++ b/internal/cloudcp/portal/dist/portal_app.css @@ -856,9 +856,17 @@ header .logout-btn:hover, display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; - margin-top: 20px; +} +.portal-support-layout { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.88fr); + gap: 14px; + margin-top: 18px; } .portal-support-card { + display: flex; + flex-direction: column; + gap: 10px; padding: 16px; border: 1px solid var(--line); border-radius: var(--radius-md); @@ -870,11 +878,70 @@ header .logout-btn:hover, letter-spacing: -0.02em; } .portal-support-card p { - margin: 10px 0 14px; + margin: 0; font-size: 14px; line-height: 1.65; color: var(--ink-soft); } +.portal-support-runbook { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: #fbfcfd; +} +.portal-support-runbook h3 { + margin: 0; + font-size: 18px; + letter-spacing: -0.02em; +} +.portal-support-runbook p { + margin: 0; + font-size: 13px; + line-height: 1.55; + color: var(--ink-soft); +} +.portal-support-runbook-list, +.portal-support-points { + display: flex; + flex-direction: column; + gap: 0; +} +.portal-support-runbook-step, +.portal-support-point { + display: flex; + flex-direction: column; + gap: 4px; + padding: 11px 0 11px 12px; + border-top: 1px solid var(--line); + border-left: 2px solid rgba(11, 106, 114, 0.14); +} +.portal-support-runbook-step:first-child, +.portal-support-point:first-child { + padding-top: 0; + border-top: none; +} +.portal-support-runbook-step strong, +.portal-support-point strong { + font-size: 12px; + line-height: 1.35; + color: var(--ink); +} +.portal-support-runbook-step span, +.portal-support-point span { + font-size: 12px; + line-height: 1.5; + color: var(--ink-soft); +} +.portal-support-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-top: auto; +} .account-stage-header { padding: 0 2px 12px; border-bottom: none; @@ -2136,6 +2203,9 @@ header .logout-btn:hover, .portal-shell-nav-group { grid-template-columns: 1fr; } + .portal-support-layout { + grid-template-columns: 1fr; + } .intro-card, .service-panel { padding: 18px; diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js index 327c404b0..3341b4fcd 100644 --- a/internal/cloudcp/portal/dist/portal_app.js +++ b/internal/cloudcp/portal/dist/portal_app.js @@ -1926,7 +1926,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 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.

'; + return '

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.

' + 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.
'; } 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 a14af88ee..95214da2b 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.test.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.test.ts @@ -211,6 +211,8 @@ describe('shell view', function() { expect(html).toContain('License desk'); expect(html).toContain('Refund desk'); expect(html).toContain('Privacy desk'); + expect(html).toContain('Route the issue cleanly'); + expect(html).toContain('Open account services'); }); it('renders self-hosted overview copy when no hosted accounts are attached', function() { diff --git a/internal/cloudcp/portal/frontend/src/shell_view.ts b/internal/cloudcp/portal/frontend/src/shell_view.ts index 30e39c17a..6e9d60cee 100644 --- a/internal/cloudcp/portal/frontend/src/shell_view.ts +++ b/internal/cloudcp/portal/frontend/src/shell_view.ts @@ -801,16 +801,43 @@ function renderSupportSection(context: ShellViewContext): string { '
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 || '') + '' + + 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.
' + + '
' + + '
' + + '' + + '
' + + '
' + '
' + - '
' + - '

Commercial services

' + - '

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

' + - '' + + '
' + + '' + + '

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.
' + + '
' + '
' + '
' + '' diff --git a/internal/cloudcp/portal/frontend/src/styles.css b/internal/cloudcp/portal/frontend/src/styles.css index ef5453238..a6922aeb1 100644 --- a/internal/cloudcp/portal/frontend/src/styles.css +++ b/internal/cloudcp/portal/frontend/src/styles.css @@ -961,10 +961,19 @@ header .logout-btn:hover, display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; - margin-top: 20px; +} + +.portal-support-layout { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.88fr); + gap: 14px; + margin-top: 18px; } .portal-support-card { + display: flex; + flex-direction: column; + gap: 10px; padding: 16px; border: 1px solid var(--line); border-radius: var(--radius-md); @@ -978,12 +987,80 @@ header .logout-btn:hover, } .portal-support-card p { - margin: 10px 0 14px; + margin: 0; font-size: 14px; line-height: 1.65; color: var(--ink-soft); } +.portal-support-runbook { + display: flex; + flex-direction: column; + gap: 12px; + padding: 16px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: #fbfcfd; +} + +.portal-support-runbook h3 { + margin: 0; + font-size: 18px; + letter-spacing: -0.02em; +} + +.portal-support-runbook p { + margin: 0; + font-size: 13px; + line-height: 1.55; + color: var(--ink-soft); +} + +.portal-support-runbook-list, +.portal-support-points { + display: flex; + flex-direction: column; + gap: 0; +} + +.portal-support-runbook-step, +.portal-support-point { + display: flex; + flex-direction: column; + gap: 4px; + padding: 11px 0 11px 12px; + border-top: 1px solid var(--line); + border-left: 2px solid rgba(11, 106, 114, 0.14); +} + +.portal-support-runbook-step:first-child, +.portal-support-point:first-child { + padding-top: 0; + border-top: none; +} + +.portal-support-runbook-step strong, +.portal-support-point strong { + font-size: 12px; + line-height: 1.35; + color: var(--ink); +} + +.portal-support-runbook-step span, +.portal-support-point span { + font-size: 12px; + line-height: 1.5; + color: var(--ink-soft); +} + +.portal-support-actions { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + margin-top: auto; +} + .account-stage-header { padding: 0 2px 12px; border-bottom: none; @@ -2440,6 +2517,10 @@ header .logout-btn:hover, grid-template-columns: 1fr; } + .portal-support-layout { + grid-template-columns: 1fr; + } + .intro-card, .service-panel { padding: 18px;