Fix sidebar showing all modules: hide Manage panel when none disabled

- App switcher: only show "Manage rApps" when there are actually
  disabled modules or active restrictions. Move "Available to Add"
  above "Remove" to prioritize adding. Eliminates duplicate module
  listing when all modules are enabled.
- Shell: update app switcher on modules-changed event (was only
  updating tab bar and folk-rapp, not the sidebar itself).
- SMTP: use space-agent@rspace.online as From for invite/approval
  emails with proper envelope sender for DKIM alignment.
- Shell CSS: fix banner z-index, smooth header transition on banner.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-01 12:32:53 -07:00
parent b54ad4b36c
commit 45372c6681
6 changed files with 47 additions and 24 deletions

View File

@ -458,6 +458,8 @@ async function executeApproval(docId: string, approvalId: string) {
replyTo: agentReplyTo, replyTo: agentReplyTo,
subject: approval.subject, subject: approval.subject,
text: approval.bodyText, text: approval.bodyText,
// Envelope override: authenticate as SMTP_USER but show mailboxEmail in From header
envelope: { from: SMTP_USER || 'noreply@rmail.online', to: approval.toAddresses },
}; };
if (approval.ccAddresses.length > 0) { if (approval.ccAddresses.length > 0) {

View File

@ -607,10 +607,19 @@ export function renderShell(opts: ShellOptions): string {
var enabledModules = detail.enabledModules; var enabledModules = detail.enabledModules;
window.__rspaceEnabledModules = enabledModules; window.__rspaceEnabledModules = enabledModules;
// Update tab bar's module list // Recompute visible + allModules with updated enabled flags
var allMods = window.__rspaceAllModules || []; var allMods = window.__rspaceAllModules || [];
var enabledSet = new Set(enabledModules); var enabledSet = new Set(enabledModules);
var visible = allMods.filter(function(m) { return m.id === 'rspace' || enabledSet.has(m.id); }); var visible = allMods.filter(function(m) { return m.id === 'rspace' || enabledSet.has(m.id); });
var updatedAll = allMods.map(function(m) { return Object.assign({}, m, { enabled: m.id === 'rspace' || enabledSet.has(m.id) }); });
window.__rspaceModuleList = visible;
window.__rspaceAllModules = updatedAll;
// Update app switcher's main navigation + catalog
var switcher = document.querySelector('rstack-app-switcher');
if (switcher) { switcher.setModules(visible); switcher.setAllModules(updatedAll); }
// Update tab bar's module list
var tb = document.querySelector('rstack-tab-bar'); var tb = document.querySelector('rstack-tab-bar');
if (tb) tb.setModules(visible); if (tb) tb.setModules(visible);

View File

@ -2199,11 +2199,14 @@ spaces.post("/:slug/invite", async (c) => {
metadata: { inviteToken: invite.token, role }, metadata: { inviteToken: invite.token, role },
}).catch(() => {}); }).catch(() => {});
// Send invite email // Send invite email from {slug}-agent@rspace.online
if (inviteTransport) { if (inviteTransport) {
try { try {
const agentAddr = `${slug}-agent@rspace.online`;
await inviteTransport.sendMail({ await inviteTransport.sendMail({
from: process.env.SMTP_FROM || "rSpace <noreply@rmail.online>", from: `${slug} <${agentAddr}>`,
replyTo: agentAddr,
envelope: { from: process.env.SMTP_USER || "noreply@rmail.online", to: body.email },
to: body.email, to: body.email,
subject: `${inviterName} invited you to "${slug}" on rSpace`, subject: `${inviterName} invited you to "${slug}" on rSpace`,
html: ` html: `
@ -2326,11 +2329,14 @@ spaces.post("/:slug/members/add", async (c) => {
metadata: { inviteToken: invite.token, role }, metadata: { inviteToken: invite.token, role },
}).catch(() => {}); }).catch(() => {});
// Send invite email (non-fatal) // Send invite email from {slug}-agent@rspace.online (non-fatal)
if (inviteTransport && targetEmail) { if (inviteTransport && targetEmail) {
try { try {
const agentAddr = `${slug}-agent@rspace.online`;
await inviteTransport.sendMail({ await inviteTransport.sendMail({
from: process.env.SMTP_FROM || "rSpace <noreply@rmail.online>", from: `${slug} <${agentAddr}>`,
replyTo: agentAddr,
envelope: { from: process.env.SMTP_USER || "noreply@rmail.online", to: targetEmail },
to: targetEmail, to: targetEmail,
subject: `${inviterName} invited you to "${slug}" on rSpace`, subject: `${inviterName} invited you to "${slug}" on rSpace`,
html: ` html: `

View File

@ -243,34 +243,37 @@ export class RStackAppSwitcher extends HTMLElement {
const disabledModules = this.#allModules.filter( const disabledModules = this.#allModules.filter(
m => m.enabled === false && m.id !== 'rspace' m => m.enabled === false && m.id !== 'rspace'
); );
if (disabledModules.length > 0 || this.#allModules.length > 0) { // Only show the Manage section when there are disabled modules to add,
// or when enabledModules is actively configured (not null/all-enabled)
const hasRestrictions = disabledModules.length > 0;
if (hasRestrictions || this.#allModules.length > this.#modules.length) {
html += ` html += `
<div class="catalog-divider"> <div class="catalog-divider">
<button class="catalog-toggle" id="catalog-toggle"> <button class="catalog-toggle" id="catalog-toggle">
${this.#catalogOpen ? '▾' : '▸'} Manage rApps ${this.#catalogOpen ? '▾' : '▸'} Manage rApps
${disabledModules.length > 0 ? `<span class="catalog-count">${disabledModules.length} available</span>` : ''} ${disabledModules.length > 0 ? `<span class="catalog-count">${disabledModules.length} more</span>` : ''}
</button> </button>
</div> </div>
`; `;
if (this.#catalogOpen) { if (this.#catalogOpen) {
html += `<div class="catalog-panel" id="catalog-panel">`; html += `<div class="catalog-panel" id="catalog-panel">`;
// Show enabled modules with toggle-off option // Show disabled modules with add option
const enabledNonCore = this.#allModules.filter(
m => m.enabled !== false && m.id !== 'rspace'
);
if (enabledNonCore.length > 0) {
html += `<div class="catalog-section-label">Enabled</div>`;
for (const m of enabledNonCore) {
html += this.#renderCatalogItem(m, true);
}
}
// Show disabled modules with toggle-on option
if (disabledModules.length > 0) { if (disabledModules.length > 0) {
html += `<div class="catalog-section-label">Available to Add</div>`; html += `<div class="catalog-section-label">Available to Add</div>`;
for (const m of disabledModules) { for (const m of disabledModules) {
html += this.#renderCatalogItem(m, false); html += this.#renderCatalogItem(m, false);
} }
} }
// Show enabled modules with remove option (compact section below)
const enabledNonCore = this.#allModules.filter(
m => m.enabled !== false && m.id !== 'rspace'
);
if (enabledNonCore.length > 0) {
html += `<div class="catalog-section-label">Remove from Space</div>`;
for (const m of enabledNonCore) {
html += this.#renderCatalogItem(m, true);
}
}
html += `</div>`; html += `</div>`;
} }
} }

View File

@ -5462,8 +5462,11 @@ app.post('/api/invites/identity', async (c) => {
const subjectLine = spaceSlug const subjectLine = spaceSlug
? `${payload.username} invited you to join "${spaceSlug}" on rSpace` ? `${payload.username} invited you to join "${spaceSlug}" on rSpace`
: `${payload.username} invited you to join rSpace`; : `${payload.username} invited you to join rSpace`;
const agentAddr = spaceSlug ? `${spaceSlug}-agent@rspace.online` : null;
await smtpTransport.sendMail({ await smtpTransport.sendMail({
from: CONFIG.smtp.from, from: agentAddr ? `${spaceSlug} <${agentAddr}>` : CONFIG.smtp.from,
...(agentAddr ? { replyTo: agentAddr } : {}),
...(agentAddr ? { envelope: { from: CONFIG.smtp.user, to: email } } : {}),
to: email, to: email,
subject: subjectLine, subject: subjectLine,
html: ` html: `

View File

@ -15,7 +15,7 @@ body {
.rspace-banner { .rspace-banner {
position: fixed; position: fixed;
top: 0; left: 0; right: 0; top: 0; left: 0; right: 0;
z-index: 10002; z-index: 10000; /* above header (9999) */
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -68,7 +68,7 @@ body {
.rspace-banner__close:hover { .rspace-banner__close:hover {
color: #fff; color: #fff;
} }
/* Push header down when a banner is visible */ /* Push header + content down when a banner is visible */
body.rspace-banner-visible .rstack-header { body.rspace-banner-visible .rstack-header {
top: 36px; top: 36px;
} }
@ -628,13 +628,13 @@ body.rspace-headers-minimized .rapp-subnav {
/* Smooth transitions for header minimize/restore */ /* Smooth transitions for header minimize/restore */
.rstack-header { .rstack-header {
transition: transform 0.25s ease; transition: transform 0.25s ease, top 0.3s ease-out;
} }
.rstack-tab-row { .rstack-tab-row {
transition: margin-left 0.25s ease, left 0.25s ease, transform 0.25s ease; transition: margin-left 0.25s ease, left 0.25s ease, transform 0.25s ease, top 0.3s ease-out;
} }
#app { #app {
transition: margin-left 0.25s ease, left 0.25s ease, padding-top 0.25s ease; transition: margin-left 0.25s ease, left 0.25s ease, padding-top 0.3s ease-out;
} }
/* Mobile: minimized state adjustments (sticky, not fixed) */ /* Mobile: minimized state adjustments (sticky, not fixed) */