mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-08 18:21:55 +00:00
style(portal): turn support into escalation desk
This commit is contained in:
parent
4c4684d446
commit
8fb55097ec
6 changed files with 195 additions and 15 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"source_hash": "5b47e0e6bc3d415732986d172946239c90f4770278b376eac60332e2e43e6e23",
|
||||
"source_hash": "feb5de02547ef564c065366222a3551f4d56198f98db27747b883bd79f938116",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
|
|
|||
74
internal/cloudcp/portal/dist/portal_app.css
vendored
74
internal/cloudcp/portal/dist/portal_app.css
vendored
|
|
@ -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;
|
||||
|
|
|
|||
2
internal/cloudcp/portal/dist/portal_app.js
vendored
2
internal/cloudcp/portal/dist/portal_app.js
vendored
|
|
@ -1926,7 +1926,7 @@
|
|||
]) + '</div><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Close team desk</button></div><div class="team-management-stats" id="team-stats-' + escapeAttr(account.id) + '"></div><div class="team-management-grid"><div class="team-roster-column"><div class="team-roster"><div class="team-panel-heading"><h4>People on this account</h4><p>Keep the roster small and role assignment explicit. The people listed here are the ones who can operate the hosted fleet.</p></div><div class="team-roster-list" id="team-list-' + escapeAttr(account.id) + '"><div class="team-list-message">Loading\u2026</div></div></div><div class="team-review-desk"><div class="team-panel-heading team-panel-heading-tight"><div class="account-panel-kicker">Review desk</div><h4>Keep access disciplined</h4><p>Use the roster as a controlled operator surface, not a dumping ground for vague shared access.</p></div><div class="team-review-grid"><div class="team-review-card"><strong>Owners stay rare</strong><span>Reserve Owner for billing, team, and full hosted control. Default to Admin, Tech, or Read-only first.</span></div><div class="team-review-card"><strong>Keep operators narrow</strong><span>Use Tech for workspace operations and Read-only for verification instead of handing out broader access.</span></div><div class="team-review-card"><strong>Remove stale access fast</strong><span>If someone no longer owns the work, remove them instead of leaving dormant access attached to the account.</span></div></div></div></div><div class="team-side-column"><div class="team-operations-panel"><div class="team-panel-heading team-panel-heading-tight"><div class="account-panel-kicker">Access desk</div><h4>Invite and role policy</h4><p>Keep the roster deliberate. Invite the smallest role first, then tighten access as responsibilities become clearer.</p></div><div class="team-operations-grid"><div class="team-invite-panel"><div class="team-panel-heading"><h4>Invite someone new</h4><p>Add another operator with the minimum role they need for this account.</p></div><div class="team-invite"><div><label for="invite-email-' + escapeAttr(account.id) + '">Email</label><input type="email" id="invite-email-' + escapeAttr(account.id) + '" placeholder="user@example.com" autocomplete="off"></div><div><label for="invite-role-' + escapeAttr(account.id) + '">Role</label><select id="invite-role-' + escapeAttr(account.id) + '"><option value="admin">Admin</option><option value="tech">Tech</option><option value="read_only">Read-only</option></select></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div>' + accessPolicy + "</div></div></div></div></section></section>";
|
||||
}
|
||||
function renderSupportSection(context) {
|
||||
return '<section class="portal-support-panel"><div class="account-panel-kicker">Support</div><h2>Support and escalation</h2><p>Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.</p><div class="portal-support-card-grid"><div class="portal-support-card"><h3>Account support</h3><p>For access, tenant handoff, team, and billing issues, contact the hosted operations desk.</p><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div><div class="portal-support-card"><h3>Commercial services</h3><p>Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.</p><button type="button" class="btn-secondary" data-shell-action="activate-section" data-shell-section="services">Open account services</button></div></div></section>';
|
||||
return '<section class="portal-support-panel"><div class="account-panel-kicker">Support</div><h2>Support and escalation</h2><p>Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.</p>' + renderSectionContextChips(["Hosted issues", "Commercial requests", context.bootstrap.support_email ? "Email" : "Support"]) + '<div class="portal-support-layout"><div class="portal-support-card-grid"><div class="portal-support-card"><div class="account-panel-kicker">Hosted account</div><h3>Account support</h3><p>Use this route when tenant handoff, workspace access, team control, or hosted billing looks wrong.</p><div class="portal-support-points"><div class="portal-support-point"><strong>Route here for hosted issues</strong><span>Access, handoff, team, and hosted billing all belong on the hosted account path.</span></div><div class="portal-support-point"><strong>Keep the account context intact</strong><span>Include the account, workspace, and action that failed so support can pick up the same operator path quickly.</span></div></div><div class="portal-support-actions"><a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || "") + '">' + escapeHTML(context.bootstrap.support_email || "") + '</a></div></div><div class="portal-support-card"><div class="account-panel-kicker">Commercial</div><h3>Commercial services</h3><p>Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the account services desk first.</p><div class="portal-support-points"><div class="portal-support-point"><strong>Start in Account services</strong><span>Use the billing, license, refund, or privacy desk before escalating a commercial issue.</span></div><div class="portal-support-point"><strong>Escalate from the same desk</strong><span>Keep the request in one place instead of splitting it between billing and operator surfaces.</span></div></div><div class="portal-support-actions"><button type="button" class="btn-secondary" data-shell-action="activate-section" data-shell-section="services">Open account services</button></div></div></div><div class="portal-support-runbook"><div class="account-panel-kicker">Escalation desk</div><h3>Route the issue cleanly</h3><p>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.</p><div class="portal-support-runbook-list"><div class="portal-support-runbook-step"><strong>1. Confirm the scope</strong><span>Decide whether the problem is hosted operations, commercial self-service, or direct support escalation.</span></div><div class="portal-support-runbook-step"><strong>2. Keep hosted and commercial separate</strong><span>Workspace and team problems stay in their own desks. Billing, license, refund, and privacy work stay in Account services.</span></div><div class="portal-support-runbook-step"><strong>3. Escalate with context</strong><span>Include the account, workspace, and exact failed action so the escalation path starts with the same facts you saw.</span></div></div></div></div></section>';
|
||||
}
|
||||
function renderHeaderHTML(context) {
|
||||
if (context.bootstrap.authenticated) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -801,16 +801,43 @@ function renderSupportSection(context: ShellViewContext): string {
|
|||
'<div class="account-panel-kicker">Support</div>' +
|
||||
'<h2>Support and escalation</h2>' +
|
||||
'<p>Use support when hosted access looks wrong, billing does not behave as expected, or you need help with commercial licensing and privacy actions.</p>' +
|
||||
'<div class="portal-support-card-grid">' +
|
||||
'<div class="portal-support-card">' +
|
||||
'<h3>Account support</h3>' +
|
||||
'<p>For access, tenant handoff, team, and billing issues, contact the hosted operations desk.</p>' +
|
||||
'<a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || '') + '">' + escapeHTML(context.bootstrap.support_email || '') + '</a>' +
|
||||
renderSectionContextChips(['Hosted issues', 'Commercial requests', context.bootstrap.support_email ? 'Email' : 'Support']) +
|
||||
'<div class="portal-support-layout">' +
|
||||
'<div class="portal-support-card-grid">' +
|
||||
'<div class="portal-support-card">' +
|
||||
'<div class="account-panel-kicker">Hosted account</div>' +
|
||||
'<h3>Account support</h3>' +
|
||||
'<p>Use this route when tenant handoff, workspace access, team control, or hosted billing looks wrong.</p>' +
|
||||
'<div class="portal-support-points">' +
|
||||
'<div class="portal-support-point"><strong>Route here for hosted issues</strong><span>Access, handoff, team, and hosted billing all belong on the hosted account path.</span></div>' +
|
||||
'<div class="portal-support-point"><strong>Keep the account context intact</strong><span>Include the account, workspace, and action that failed so support can pick up the same operator path quickly.</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="portal-support-actions">' +
|
||||
'<a class="portal-support-link" href="mailto:' + escapeAttr(context.bootstrap.support_email || '') + '">' + escapeHTML(context.bootstrap.support_email || '') + '</a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="portal-support-card">' +
|
||||
'<div class="account-panel-kicker">Commercial</div>' +
|
||||
'<h3>Commercial services</h3>' +
|
||||
'<p>Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the account services desk first.</p>' +
|
||||
'<div class="portal-support-points">' +
|
||||
'<div class="portal-support-point"><strong>Start in Account services</strong><span>Use the billing, license, refund, or privacy desk before escalating a commercial issue.</span></div>' +
|
||||
'<div class="portal-support-point"><strong>Escalate from the same desk</strong><span>Keep the request in one place instead of splitting it between billing and operator surfaces.</span></div>' +
|
||||
'</div>' +
|
||||
'<div class="portal-support-actions">' +
|
||||
'<button type="button" class="btn-secondary" data-shell-action="activate-section" data-shell-section="services">Open account services</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="portal-support-card">' +
|
||||
'<h3>Commercial services</h3>' +
|
||||
'<p>Self-hosted subscriptions, license recovery, refunds, and privacy requests all route through the same account surface.</p>' +
|
||||
'<button type="button" class="btn-secondary" data-shell-action="activate-section" data-shell-section="services">Open account services</button>' +
|
||||
'<div class="portal-support-runbook">' +
|
||||
'<div class="account-panel-kicker">Escalation desk</div>' +
|
||||
'<h3>Route the issue cleanly</h3>' +
|
||||
'<p>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.</p>' +
|
||||
'<div class="portal-support-runbook-list">' +
|
||||
'<div class="portal-support-runbook-step"><strong>1. Confirm the scope</strong><span>Decide whether the problem is hosted operations, commercial self-service, or direct support escalation.</span></div>' +
|
||||
'<div class="portal-support-runbook-step"><strong>2. Keep hosted and commercial separate</strong><span>Workspace and team problems stay in their own desks. Billing, license, refund, and privacy work stay in Account services.</span></div>' +
|
||||
'<div class="portal-support-runbook-step"><strong>3. Escalate with context</strong><span>Include the account, workspace, and exact failed action so the escalation path starts with the same facts you saw.</span></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</section>'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue