feat(uat-batch): Groups J + K — activity feed + onboarding resolver-chain

J38, J39, K40 (core) from the 2026-05-21 plan.

Shipped:
  J38  EntityActivityFeed sentence rendering surfaces the new value
       inline. Was "<actor> updated the X"; now "<actor> set X to
       <value>" when the audit row carries `newValue`. Field-level
       diff line underneath keeps showing the old → new strikethrough
       for context. Truncates inline value at 60 chars to keep long
       notes / descriptions from blowing out the row.
  J39  Client → Companies tab CTA. Empty state gains a "Link to a
       company" action; populated state grows a top-right "Link to
       company" button. New <LinkCompanyDialog> wraps the existing
       <CompanyPicker> + a membership-role select + an "is primary"
       checkbox, then POSTs to /api/v1/companies/[id]/members.
       Empty-state copy dropped "Add a membership from a company's
       detail page" — the rep can act inline now.
  K40  OnboardingChecklist resolver-chain. The auto-check no longer
       reads raw `/admin/settings` rows (which miss env fallbacks).
       Resolved endpoint widened to accept `?keys=k1,k2,...` so the
       checklist can batch-resolve any heterogenous set of registry
       keys through port → global → env → default in one round-trip.
       Checklist captures the dominant source per step ("env fallback",
       "global default", "built-in default") and surfaces it inline
       under the green tick so super-admins see when a step is
       relying on env rather than a per-port override. Compound-key
       gates report the weakest sub-key's source so a partially-env
       config still flags clearly.
       Topbar banner / dashboard tile / weekly nudge / celebration
       sub-items remain queued — the core resolver-chain gap was
       the actual cause of the "step never ticks" UAT complaint.

Verified: tsc clean, vitest 1454/1454.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 23:02:33 +02:00
parent 989cc4d72b
commit 03a7521729
4 changed files with 293 additions and 29 deletions

View File

@@ -2,16 +2,20 @@ import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { entriesForSections } from '@/lib/settings/registry';
import { entriesForSections, registryFor } from '@/lib/settings/registry';
import { resolveForAdminAPI } from '@/lib/settings/resolver';
/**
* GET /api/v1/admin/settings/resolved?sections=documenso.api,documenso.signers
* GET /api/v1/admin/settings/resolved?keys=branding_logo_url,smtp_host_override
*
* Returns the resolved value + source (port/global/env/default) for every
* registry entry in the requested sections. Drives the registry-driven
* admin form: the `source` field gates the "Using env fallback" badge.
* requested registry entry. Drives both the registry-driven admin form
* (sections param) and the onboarding-checklist auto-detection (keys
* param) — both need port→global→env→default resolution rather than the
* raw `/admin/settings` rows (which only show DB writes).
*
* Either parameter is supported; if both are present the sets union.
* Sensitive fields surface `isSet` only — never the decrypted value.
*/
export const GET = withAuth(
@@ -19,14 +23,33 @@ export const GET = withAuth(
try {
const url = new URL(req.url);
const sectionsParam = url.searchParams.get('sections');
if (!sectionsParam) {
const keysParam = url.searchParams.get('keys');
if (!sectionsParam && !keysParam) {
return NextResponse.json({ data: { entries: [], values: {} } }, { status: 200 });
}
const sections = sectionsParam
.split(',')
.map((s) => s.trim())
.filter(Boolean);
const entries = entriesForSections(sections);
? sectionsParam
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: [];
const extraKeys = keysParam
? keysParam
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: [];
const sectionEntries = entriesForSections(sections);
const keyEntries = extraKeys
.map((k) => registryFor(k))
.filter((e): e is NonNullable<typeof e> => Boolean(e));
// Dedupe by `key` so section + key overlap doesn't double-resolve.
const seen = new Set<string>();
const entries = [...sectionEntries, ...keyEntries].filter((e) => {
if (seen.has(e.key)) return false;
seen.add(e.key);
return true;
});
const keys = entries.map((e) => e.key);
const resolved = await resolveForAdminAPI(keys, ctx.portId);