mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
chore(portal): remove accidental recovery spillover
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source_hash": "610586b6afe868a59ea40e626d2d5af557bb144053f5a98dfa06ee7394b30625",
|
||||
"source_hash": "e0dc04d4f7be0d3c9048256e3dfff63acb71c236f2c8fd44b237612191a7d696",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
||||
+23
-27
@@ -1165,26 +1165,42 @@ header .logout-btn:hover,
|
||||
.team-side-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
align-self: start;
|
||||
}
|
||||
.team-roster,
|
||||
.team-invite-panel {
|
||||
.team-invite-panel,
|
||||
.team-operations-panel {
|
||||
background: #fff;
|
||||
border-color: var(--line);
|
||||
}
|
||||
.team-roster {
|
||||
align-self: start;
|
||||
}
|
||||
.team-policy-panel {
|
||||
.team-operations-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
.team-operations-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.team-policy-panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-muted);
|
||||
padding: 14px;
|
||||
}
|
||||
.team-panel-heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.team-panel-heading-tight {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.team-panel-heading p {
|
||||
max-width: 720px;
|
||||
}
|
||||
@@ -1282,38 +1298,18 @@ header .logout-btn:hover,
|
||||
}
|
||||
.team-list-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 144px;
|
||||
padding: 18px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--line-strong);
|
||||
border-radius: 10px;
|
||||
color: var(--ink-muted);
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
.team-list-message-title {
|
||||
font-size: 15px;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.team-list-message-copy {
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.team-list-message.error {
|
||||
border-color: #efc1c1;
|
||||
background: #fff4f4;
|
||||
}
|
||||
.team-list-message.error .team-list-message-title {
|
||||
color: var(--danger);
|
||||
}
|
||||
.team-list-message.error .team-list-message-copy {
|
||||
color: #8a4141;
|
||||
}
|
||||
.btn-remove {
|
||||
padding: 0;
|
||||
background: none;
|
||||
|
||||
+11
-18
@@ -138,19 +138,12 @@
|
||||
actionButton.setAttribute("data-workspace-action", workspace.state === "active" ? "suspend" : "delete");
|
||||
closeButton.disabled = entry.manageWorkspace.pending;
|
||||
}
|
||||
function setContainerMessage(container, title, msg, isError) {
|
||||
function setContainerMessage(container, msg, isError) {
|
||||
container.textContent = "";
|
||||
var card = document.createElement("div");
|
||||
card.className = "team-list-message" + (isError ? " error" : "");
|
||||
var titleNode = document.createElement("strong");
|
||||
titleNode.className = "team-list-message-title";
|
||||
titleNode.textContent = title;
|
||||
card.appendChild(titleNode);
|
||||
var body = document.createElement("span");
|
||||
body.className = "team-list-message-copy";
|
||||
body.textContent = msg;
|
||||
card.appendChild(body);
|
||||
container.appendChild(card);
|
||||
var message = document.createElement("div");
|
||||
message.className = "team-list-message" + (isError ? " error" : "");
|
||||
message.textContent = msg;
|
||||
container.appendChild(message);
|
||||
}
|
||||
function countMembersByRole(members, role) {
|
||||
var count = 0;
|
||||
@@ -167,11 +160,11 @@
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === "loading") {
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value">Loading\u2026</span></div><div class="team-stat-card"><span class="team-stat-label">Invites</span><span class="team-stat-value">Ready</span></div>';
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value">Loading\u2026</span></div>';
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === "error") {
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value team-stat-error">Needs attention</span></div><div class="team-stat-card"><span class="team-stat-label">Fallback</span><span class="team-stat-value">Invite only</span></div>';
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value team-stat-error">Needs attention</span></div>';
|
||||
return;
|
||||
}
|
||||
var members = entry.teamQuery.data;
|
||||
@@ -272,15 +265,15 @@
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === "loading") {
|
||||
setContainerMessage(roster, "Loading roster", "Checking who currently has access to this account.", false);
|
||||
setContainerMessage(roster, "Loading\u2026", false);
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === "error") {
|
||||
setContainerMessage(roster, "Roster needs attention", entry.teamQuery.error, true);
|
||||
setContainerMessage(roster, entry.teamQuery.error, true);
|
||||
return;
|
||||
}
|
||||
if (!entry.teamQuery.data.length) {
|
||||
setContainerMessage(roster, "No operators yet", "Invite someone new when this hosted account needs shared access.", false);
|
||||
setContainerMessage(roster, "No team members.", false);
|
||||
return;
|
||||
}
|
||||
roster.textContent = "";
|
||||
@@ -1865,7 +1858,7 @@
|
||||
}
|
||||
function renderAccountTeamSection(account) {
|
||||
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>";
|
||||
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-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>';
|
||||
|
||||
@@ -83,13 +83,12 @@ describe('account view', function() {
|
||||
teamVisible: true,
|
||||
teamQuery: { status: 'loading', error: '', data: [] },
|
||||
}));
|
||||
expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Loading roster');
|
||||
expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Loading');
|
||||
|
||||
renderTeamSection('acct_1', createEntry({
|
||||
teamVisible: true,
|
||||
teamQuery: { status: 'error', error: 'Failed to load team.', data: [] },
|
||||
}));
|
||||
expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Roster needs attention');
|
||||
expect(document.getElementById('team-list-acct_1')?.textContent).toContain('Failed to load team.');
|
||||
|
||||
renderTeamSection(
|
||||
|
||||
@@ -163,22 +163,12 @@ export function renderWorkspaceManagement(account: PortalAccountSummary, entry:
|
||||
closeButton.disabled = entry.manageWorkspace.pending;
|
||||
}
|
||||
|
||||
function setContainerMessage(container: HTMLElement, title: string, msg: string, isError: boolean): void {
|
||||
function setContainerMessage(container: HTMLElement, msg: string, isError: boolean): void {
|
||||
container.textContent = '';
|
||||
var card = document.createElement('div');
|
||||
card.className = 'team-list-message' + (isError ? ' error' : '');
|
||||
|
||||
var titleNode = document.createElement('strong');
|
||||
titleNode.className = 'team-list-message-title';
|
||||
titleNode.textContent = title;
|
||||
card.appendChild(titleNode);
|
||||
|
||||
var body = document.createElement('span');
|
||||
body.className = 'team-list-message-copy';
|
||||
body.textContent = msg;
|
||||
card.appendChild(body);
|
||||
|
||||
container.appendChild(card);
|
||||
var message = document.createElement('div');
|
||||
message.className = 'team-list-message' + (isError ? ' error' : '');
|
||||
message.textContent = msg;
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
function countMembersByRole(members: PortalTeamMember[], role: string): number {
|
||||
@@ -197,15 +187,11 @@ function renderTeamStats(accountID: string, entry: PortalAccountUIEntry): void {
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === 'loading') {
|
||||
stats.innerHTML =
|
||||
'<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value">Loading…</span></div>' +
|
||||
'<div class="team-stat-card"><span class="team-stat-label">Invites</span><span class="team-stat-value">Ready</span></div>';
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value">Loading…</span></div>';
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === 'error') {
|
||||
stats.innerHTML =
|
||||
'<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value team-stat-error">Needs attention</span></div>' +
|
||||
'<div class="team-stat-card"><span class="team-stat-label">Fallback</span><span class="team-stat-value">Invite only</span></div>';
|
||||
stats.innerHTML = '<div class="team-stat-card"><span class="team-stat-label">Roster</span><span class="team-stat-value team-stat-error">Needs attention</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,15 +317,15 @@ export function renderTeamSection(accountID: string, entry: PortalAccountUIEntry
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === 'loading') {
|
||||
setContainerMessage(roster, 'Loading roster', 'Checking who currently has access to this account.', false);
|
||||
setContainerMessage(roster, 'Loading…', false);
|
||||
return;
|
||||
}
|
||||
if (entry.teamQuery.status === 'error') {
|
||||
setContainerMessage(roster, 'Roster needs attention', entry.teamQuery.error, true);
|
||||
setContainerMessage(roster, entry.teamQuery.error, true);
|
||||
return;
|
||||
}
|
||||
if (!entry.teamQuery.data.length) {
|
||||
setContainerMessage(roster, 'No operators yet', 'Invite someone new when this hosted account needs shared access.', false);
|
||||
setContainerMessage(roster, 'No team members.', false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1311,12 +1311,12 @@ header .logout-btn:hover,
|
||||
.team-side-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.team-roster,
|
||||
.team-invite-panel {
|
||||
.team-invite-panel,
|
||||
.team-operations-panel {
|
||||
background: #fff;
|
||||
border-color: var(--line);
|
||||
}
|
||||
@@ -1325,17 +1325,36 @@ header .logout-btn:hover,
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.team-policy-panel {
|
||||
.team-operations-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.team-operations-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.team-policy-panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-muted);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.team-panel-heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.team-panel-heading-tight {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.team-panel-heading p {
|
||||
max-width: 720px;
|
||||
}
|
||||
@@ -1446,44 +1465,20 @@ header .logout-btn:hover,
|
||||
|
||||
.team-list-message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 144px;
|
||||
padding: 18px;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: 1px dashed var(--line-strong);
|
||||
border-radius: 10px;
|
||||
color: var(--ink-muted);
|
||||
background: var(--panel-muted);
|
||||
}
|
||||
|
||||
.team-list-message-title {
|
||||
font-size: 15px;
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.team-list-message-copy {
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.team-list-message.error {
|
||||
border-color: #efc1c1;
|
||||
background: #fff4f4;
|
||||
}
|
||||
|
||||
.team-list-message.error .team-list-message-title {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.team-list-message.error .team-list-message-copy {
|
||||
color: #8a4141;
|
||||
}
|
||||
|
||||
.btn-remove {
|
||||
padding: 0;
|
||||
background: none;
|
||||
|
||||
Reference in New Issue
Block a user