style(portal): strengthen support routing desk

This commit is contained in:
rcourtman 2026-03-28 08:03:07 +00:00
parent 309d985092
commit fb216d8ea7
6 changed files with 145 additions and 17 deletions

View file

@ -1,5 +1,5 @@
{
"source_hash": "1d790655efb7b0c07c4a10f41a3814aa2313d2da5a5b387f40fbf1a2aae1e8c5",
"source_hash": "eb680f19bec2eebdbbc08c03606a9d42235e1f76d8d69a33f438dfd5ede4773c",
"build_inputs": [
"package.json",
"tsconfig.json",

View file

@ -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;
}

View file

@ -1942,7 +1942,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 class="team-review-strip"><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><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 desk</h2><p>Use this desk when hosted access looks wrong, billing behaves unexpectedly, or you need help with commercial requests.</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>';
return '<section class="portal-support-panel"><div class="account-panel-kicker">Support</div><h2>Support desk</h2><p>Use this desk when hosted access looks wrong, billing behaves unexpectedly, or you need help with commercial requests.</p>' + renderSectionContextChips(["Hosted issues", "Commercial requests", context.bootstrap.support_email ? "Email" : "Support"]) + '<div class="portal-support-brief-strip"><div class="portal-support-brief-card"><strong>Hosted path</strong><span>Workspace access, team control, tenant handoff, and hosted billing stay on the hosted account route.</span></div><div class="portal-support-brief-card"><strong>Commercial path</strong><span>Self-hosted billing, licenses, refunds, and privacy requests stay in Account services until escalation is needed.</span></div><div class="portal-support-brief-card"><strong>Escalate with context</strong><span>Include the exact account, workspace, desk, and failed action so support can continue the same path immediately.</span></div></div><div class="portal-support-layout"><div class="portal-support-route-grid"><div class="portal-support-route-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-route-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 class="portal-support-handoff-note"><strong>Include in the escalation</strong><span>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.</span></div></div></div></section>';
}
function renderHeaderHTML(context) {
if (context.bootstrap.authenticated) {

View file

@ -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');
});

View file

@ -840,9 +840,23 @@ function renderSupportSection(context: ShellViewContext): string {
'<h2>Support desk</h2>' +
'<p>Use this desk when hosted access looks wrong, billing behaves unexpectedly, or you need help with commercial requests.</p>' +
renderSectionContextChips(['Hosted issues', 'Commercial requests', context.bootstrap.support_email ? 'Email' : 'Support']) +
'<div class="portal-support-brief-strip">' +
'<div class="portal-support-brief-card">' +
'<strong>Hosted path</strong>' +
'<span>Workspace access, team control, tenant handoff, and hosted billing stay on the hosted account route.</span>' +
'</div>' +
'<div class="portal-support-brief-card">' +
'<strong>Commercial path</strong>' +
'<span>Self-hosted billing, licenses, refunds, and privacy requests stay in Account services until escalation is needed.</span>' +
'</div>' +
'<div class="portal-support-brief-card">' +
'<strong>Escalate with context</strong>' +
'<span>Include the exact account, workspace, desk, and failed action so support can continue the same path immediately.</span>' +
'</div>' +
'</div>' +
'<div class="portal-support-layout">' +
'<div class="portal-support-card-grid">' +
'<div class="portal-support-card">' +
'<div class="portal-support-route-grid">' +
'<div class="portal-support-route-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>' +
@ -854,7 +868,7 @@ function renderSupportSection(context: ShellViewContext): string {
'<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="portal-support-route-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>' +
@ -876,6 +890,10 @@ function renderSupportSection(context: ShellViewContext): string {
'<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 class="portal-support-handoff-note">' +
'<strong>Include in the escalation</strong>' +
'<span>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.</span>' +
'</div>' +
'</div>' +
'</div>' +
'</section>'

View file

@ -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;
}