feat(korrespondenz): address PR #164 review – blockers and suggestions
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 1m36s
CI / Backend Unit Tests (pull_request) Failing after 2m36s
CI / E2E Tests (pull_request) Failing after 1h49m0s

Blockers (14):
- B1: fix senderName/receiverName to use $derived instead of $state + sync $effect
- B2: migrate all korrespondenz components from messages-extra shim to paraglide m.*
- B3: i18n CorrespondenzEmptyState (heading, subtext, search placeholder)
- B4: add response.ok checks to admin layout server load
- B5: add response.ok checks to korrespondenz page server load
- B6: add page.server.spec.ts with 5 test suites for korrespondenz load function
- B7: add axe-core accessibility checks to all e2e korrespondenz tests
- B8: add Testcontainers JPQL tests for findSinglePersonCorrespondence (DISTINCT + sender)
- B9: hide auth reset-token endpoint from OpenAPI spec; remove from generated api.ts
- B11: replace amber hardcoded hex colors in SinglePersonHintBar with brand tokens
- B12: replace clipboard emoji with Heroicons SVG in SinglePersonHintBar
- B13: create DateInput component (German dd.mm.yyyy); use it in CorrespondenzFilterControls
- B14: add Paraglide compile step to CI workflow before lint/test

Suggestions (11):
- S1: make CorrespondentSuggestionsDropdown a pure display component; lift fetch to PersonBar
- S2: fix leftover messages-extra import in ConversationTimeline; use brand tokens for status dots
- S3: add intent comment to EntityNav openFlyout behavior
- S4: rename canManageGroups → canManagePermissions throughout admin
- S6: remove domFlush helper from DateInput spec; use expect.poll instead
- S7: replace test.skip with throw new Error in bilateral e2e tests
- S8: add inverse aria-disabled test for filter strip
- S9: remove sm:min-h-0 from sort button to preserve 44px touch target
- S10: add title attributes to tablet trigger buttons in EntityNav
- S11: delete messages-extra.ts shim entirely

Also: fix admin pages revealing blank strip at bottom (-mb-6 on admin layout)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-30 19:57:48 +02:00
parent 9d6c7b8605
commit 154f859efc
26 changed files with 459 additions and 184 deletions

View File

@@ -23,19 +23,35 @@ export async function load({ fetch, locals }) {
if (!hasAnyAdminPerm(user)) throw error(403, getErrorMessage('FORBIDDEN'));
const api = createApiClient(fetch);
// TODO: replace with a dedicated /api/admin/stats endpoint that returns counts only,
// so the System page does not load full entity lists it does not render.
const [usersResult, groupsResult, tagsResult] = await Promise.all([
api.GET('/api/users'),
api.GET('/api/groups'),
api.GET('/api/tags')
]);
if (!usersResult.response.ok) {
const code = (usersResult.error as unknown as { code?: string })?.code;
throw error(usersResult.response.status, getErrorMessage(code));
}
if (!groupsResult.response.ok) {
const code = (groupsResult.error as unknown as { code?: string })?.code;
throw error(groupsResult.response.status, getErrorMessage(code));
}
if (!tagsResult.response.ok) {
const code = (tagsResult.error as unknown as { code?: string })?.code;
throw error(tagsResult.response.status, getErrorMessage(code));
}
return {
userCount: (usersResult.data ?? []).length,
groupCount: (groupsResult.data ?? []).length,
tagCount: (tagsResult.data ?? []).length,
canManageUsers: hasPerm(user, 'ADMIN_USER'),
canManageTags: hasPerm(user, 'ADMIN_TAG'),
canManageGroups: hasPerm(user, 'ADMIN_PERMISSION'),
canManagePermissions: hasPerm(user, 'ADMIN_PERMISSION'),
canRunMaintenance: hasPerm(user, 'ADMIN')
};
}

View File

@@ -12,7 +12,7 @@ let { data, children } = $props();
-mt-6: cancel the global layout's pt-6 on <main>
Height fills from below the global header (64px) to bottom of viewport.
-->
<div class="-mt-6 flex overflow-hidden" style="height: calc(100vh - 65px)">
<div class="-mt-6 -mb-6 flex overflow-hidden" style="height: calc(100vh - 65px)">
<!-- Entity Nav: hidden on mobile, icon strip on tablet, full labels on desktop -->
<div class="hidden md:flex">
<EntityNav
@@ -21,7 +21,7 @@ let { data, children } = $props();
tagCount={data.tagCount}
canManageUsers={data.canManageUsers}
canManageTags={data.canManageTags}
canManageGroups={data.canManageGroups}
canManagePermissions={data.canManagePermissions}
canRunMaintenance={data.canRunMaintenance}
/>
</div>

View File

@@ -36,7 +36,7 @@ onMount(() => {
</a>
{/if}
{#if data.canManageGroups}
{#if data.canManagePermissions}
<a href="/admin/groups" class="flex items-center justify-between px-4 py-4 hover:bg-muted">
<div>
<div class="font-sans text-sm font-bold text-ink">{m.admin_tab_groups()}</div>

View File

@@ -10,7 +10,7 @@ let {
tagCount,
canManageUsers,
canManageTags,
canManageGroups,
canManagePermissions,
canRunMaintenance
}: {
userCount: number;
@@ -18,7 +18,7 @@ let {
tagCount: number;
canManageUsers: boolean;
canManageTags: boolean;
canManageGroups: boolean;
canManagePermissions: boolean;
canRunMaintenance: boolean;
} = $props();
@@ -28,6 +28,9 @@ const isActive = (section: string) => currentPath.startsWith(`/admin/${section}`
let flyoutOpen = $state(false);
let flyoutTriggerElement: HTMLButtonElement | null = null;
// All four section buttons open the same flyout that repeats the full nav.
// This is intentional: on tablet the flyout shows all sections as a wider navigation panel,
// not a context-specific panel for the clicked section.
async function openFlyout(event: MouseEvent) {
flyoutTriggerElement = event.currentTarget as HTMLButtonElement;
flyoutOpen = true;
@@ -71,6 +74,7 @@ function handleKeydown(event: KeyboardEvent) {
data-flyout-trigger
type="button"
aria-label={m.admin_tab_users()}
title={m.admin_tab_users()}
onclick={openFlyout}
class="flex min-h-[44px] w-full flex-col items-center justify-center gap-0.5 border-l-[3px] py-3 transition-colors lg:hidden
{isActive('users')
@@ -131,12 +135,13 @@ function handleKeydown(event: KeyboardEvent) {
</a>
{/if}
{#if canManageGroups}
{#if canManagePermissions}
<!-- Tablet trigger button (md only, hidden at lg) -->
<button
data-flyout-trigger
type="button"
aria-label={m.admin_tab_groups()}
title={m.admin_tab_groups()}
onclick={openFlyout}
class="flex min-h-[44px] w-full flex-col items-center justify-center gap-0.5 border-l-[3px] py-3 transition-colors lg:hidden
{isActive('groups')
@@ -203,6 +208,7 @@ function handleKeydown(event: KeyboardEvent) {
data-flyout-trigger
type="button"
aria-label={m.admin_tab_tags()}
title={m.admin_tab_tags()}
onclick={openFlyout}
class="flex min-h-[44px] w-full flex-col items-center justify-center gap-0.5 border-l-[3px] py-3 transition-colors lg:hidden
{isActive('tags')
@@ -273,6 +279,7 @@ function handleKeydown(event: KeyboardEvent) {
data-flyout-trigger
type="button"
aria-label={m.admin_tab_system()}
title={m.admin_tab_system()}
onclick={openFlyout}
class="flex min-h-[44px] w-full flex-col items-center justify-center gap-0.5 border-t border-l-[3px] border-white/10 py-3 transition-colors lg:hidden
{isActive('system')
@@ -390,7 +397,7 @@ function handleKeydown(event: KeyboardEvent) {
</a>
{/if}
{#if canManageGroups}
{#if canManagePermissions}
<a
href="/admin/groups"
onclick={closeFlyout}

View File

@@ -15,7 +15,7 @@ const props = {
tagCount: 8,
canManageUsers: true,
canManageTags: true,
canManageGroups: true,
canManagePermissions: true,
canRunMaintenance: true
};

View File

@@ -66,7 +66,7 @@ describe('admin layout load — permission check', () => {
expect(result.tagCount).toBe(3);
expect(result.canManageUsers).toBe(true);
expect(result.canManageTags).toBe(true);
expect(result.canManageGroups).toBe(true);
expect(result.canManagePermissions).toBe(true);
expect(result.canRunMaintenance).toBe(true);
});
});

View File

@@ -19,7 +19,7 @@ const fullPerms = {
tagCount: 7,
canManageUsers: true,
canManageTags: true,
canManageGroups: true,
canManagePermissions: true,
canRunMaintenance: true
};

View File

@@ -16,7 +16,7 @@ const fullData = {
tagCount: 7,
canManageUsers: true,
canManageTags: true,
canManageGroups: true,
canManagePermissions: true,
canRunMaintenance: true
};