style(portal): add team access policy column

This commit is contained in:
rcourtman
2026-03-27 18:54:31 +00:00
parent 0ffec003d1
commit 72d9eace5f
6 changed files with 91 additions and 41 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "8b0b284da60918f341d35eb2e51aed2893c7af4b399d949fd6dc0cef3e166408",
"source_hash": "06fe7351022ab56b379fb5059391505df0cd8b1ddb0c897f4b7ba30a9cca6866",
"build_inputs": [
"package.json",
"tsconfig.json",
+24 -8
View File
@@ -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);
+2 -1
View File
@@ -1813,7 +1813,8 @@
return '<section class="account-content-panel account-content-panel-workspaces"><div class="account-stage-header"><div><div class="account-panel-kicker">Workspace fleet</div><h3>Hosted fleet</h3><p>Use the fleet view to open workspaces, watch health posture, and keep management actions explicit.</p></div></div><div class="workspace-operations-shell"><div class="workspace-operations-main">' + workspaceHTML + '</div><div class="workspace-operations-detail">' + workspaceManagement + "</div></div></section>";
}
function renderAccountTeamSection(account) {
return '<section class="account-content-panel account-content-panel-team"><section class="team-management-panel team-section team-section-shell" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><div class="team-management-header"><div><div class="account-panel-kicker">Team management</div><h3>Control who can operate this account</h3><p>Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.</p></div><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Done</button></div><div class="team-management-stats" id="team-stats-' + escapeAttr(account.id) + '"></div><div class="team-management-grid"><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-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><div class="team-invite-guide"><div class="team-invite-guide-row"><strong>Admin</strong><span>Billing and hosted operations.</span></div><div class="team-invite-guide-row"><strong>Tech</strong><span>Operational access without billing control.</span></div><div class="team-invite-guide-row"><strong>Read-only</strong><span>Review hosted state without making changes.</span></div></div><button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' + escapeAttr(account.id) + '">Invite</button></div></div></div></section></section>';
var accessPolicy = '<div class="team-policy-panel"><div class="team-panel-heading"><h4>Access model</h4><p>Assign the smallest role that still lets someone do the work they own on this account.</p></div><div class="team-policy-list"><div class="team-policy-row"><strong>Owner</strong><span>Billing, team access, and full hosted control.</span></div><div class="team-policy-row"><strong>Admin</strong><span>Hosted operations plus billing for the account.</span></div><div class="team-policy-row"><strong>Tech</strong><span>Workspace operations without billing ownership.</span></div><div class="team-policy-row"><strong>Read-only</strong><span>State review and verification without control-plane changes.</span></div></div></div>';
return '<section class="account-content-panel account-content-panel-team"><section class="team-management-panel team-section team-section-shell" id="team-section-' + escapeAttr(account.id) + '" data-actor-role="' + escapeAttr(account.role) + '"><div class="team-management-header"><div><div class="account-panel-kicker">Team management</div><h3>Control who can operate this account</h3><p>Owners manage billing and access. Admins and techs keep the hosted fleet running day to day.</p></div><button type="button" class="btn-secondary btn-compact" data-shell-action="activate-section" data-shell-section="workspaces">Done</button></div><div class="team-management-stats" id="team-stats-' + escapeAttr(account.id) + '"></div><div class="team-management-grid"><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-side-column"><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></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>';
@@ -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');
@@ -579,6 +579,20 @@ function renderAccountWorkspaceSection(account: PortalAccountSummary, accountAPI
}
function renderAccountTeamSection(account: PortalAccountSummary): string {
var accessPolicy =
'<div class="team-policy-panel">' +
'<div class="team-panel-heading">' +
'<h4>Access model</h4>' +
'<p>Assign the smallest role that still lets someone do the work they own on this account.</p>' +
'</div>' +
'<div class="team-policy-list">' +
'<div class="team-policy-row"><strong>Owner</strong><span>Billing, team access, and full hosted control.</span></div>' +
'<div class="team-policy-row"><strong>Admin</strong><span>Hosted operations plus billing for the account.</span></div>' +
'<div class="team-policy-row"><strong>Tech</strong><span>Workspace operations without billing ownership.</span></div>' +
'<div class="team-policy-row"><strong>Read-only</strong><span>State review and verification without control-plane changes.</span></div>' +
'</div>' +
'</div>';
return (
'<section class="account-content-panel account-content-panel-team">' +
'<section class="team-management-panel team-section team-section-shell" id="team-section-' +
@@ -609,31 +623,29 @@ function renderAccountTeamSection(account: PortalAccountSummary): string {
'<div class="team-list-message">Loading…</div>' +
'</div>' +
'</div>' +
'<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>' +
'<div class="team-invite-guide">' +
'<div class="team-invite-guide-row"><strong>Admin</strong><span>Billing and hosted operations.</span></div>' +
'<div class="team-invite-guide-row"><strong>Tech</strong><span>Operational access without billing control.</span></div>' +
'<div class="team-invite-guide-row"><strong>Read-only</strong><span>Review hosted state without making changes.</span></div>' +
'<div class="team-side-column">' +
'<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>' +
'<button type="button" class="btn-primary btn-compact" data-action="invite-member" data-account-id="' +
escapeAttr(account.id) +
'">Invite</button>' +
'</div>' +
accessPolicy +
'</div>' +
'</div>' +
'</section>' +
@@ -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);