feat(reports-p7): cover-page brand picker (admin-only)

- DashboardReportBuilder grows an optional Cover-page brand picker
  surfaced only when can('admin', 'manage_settings') AND the user has
  access to >1 port. Pulls ports from PortContext; default option is
  "Use active port brand", remaining options are the other ports the
  user can reach. Choice persists in config.coverBrandPortId; threaded
  through preview, download (/reports/generate), and queue
  (/reports/runs) payloads.
- render-report.service.ts: when run.config.coverBrandPortId resolves
  to an accessible port, the cover-page logo + portName come from THAT
  port's brand kit. Falls back to the source port silently when the
  override port is missing or stale. Source-port DATA stays — only the
  cover branding swaps. Useful for cross-port leadership decks.

Verified: tsc clean, 1493/1493 vitest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 17:18:00 +02:00
parent d32e557e56
commit 8998f68c0f
2 changed files with 70 additions and 4 deletions

View File

@@ -202,11 +202,26 @@ export async function renderReportRun(reportRunId: string): Promise<ReportRun> {
throw new Error(`Cannot render report ${run.id}: port ${run.portId} not found`);
}
const logo = await resolvePortLogo(run.portId).catch(() => ({
// P7: optional cover-brand swap. When config.coverBrandPortId points
// at a port the rep has access to, the cover-page logo + port name
// come from THAT port's brand kit instead of the report's source
// port. Useful for cross-port leadership decks; falls back to the
// source port when the override port is missing / inaccessible.
const params = (run.config as Record<string, unknown>) ?? {};
const overrideBrandPortId =
typeof params.coverBrandPortId === 'string' && params.coverBrandPortId.length > 0
? params.coverBrandPortId
: null;
const brandPortId = overrideBrandPortId ?? run.portId;
const brandPort =
overrideBrandPortId === null
? port
: ((await db.query.ports.findFirst({ where: eq(ports.id, brandPortId) })) ?? port);
const logo = await resolvePortLogo(brandPort.id).catch(() => ({
buffer: null as Buffer | null,
}));
const ctx: RenderCtx = { portName: port.name, logoBuffer: logo.buffer ?? null };
const params = (run.config as Record<string, unknown>) ?? {};
const ctx: RenderCtx = { portName: brandPort.name, logoBuffer: logo.buffer ?? null };
const data = await renderer.fetchData(run.portId, params);