From aef40bb4250e250075f02bd12fe965d9a181eaaa Mon Sep 17 00:00:00 2001 From: m Date: Thu, 30 Apr 2026 02:29:09 +0200 Subject: [PATCH] feat(t-paliad-073): audit polish-2 DEFER list cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six findings from docs/audit-polish-2-2026-04-29.md DEFER list: - F-23: hide STATUS column on /deadlines + /projects when every visible row shares the same status. Toggled at render time via a CSS class on the table; the column re-appears the moment filters re-introduce variety. - F-32: agenda urgency pill now renders only when it disagrees with the day-bucket heading (e.g. an Überfällig deadline that lands in HEUTE through a filter quirk). Common case drops the redundant tag. - F-38: bottom-nav agenda badge already counted overdue+today (the brief's option (b)); added a localized title + aria-label so the count's semantics ("X überfällig + Y heute fällig") is no longer ambiguous. - F-40: glossary filter chips no longer mix EN+DE — DE shows "Streitsachen / Erteilungsverfahren / Allgemein", EN keeps "Litigation / Prosecution / General". Same i18n keys cover the Suggest-modal category dropdown. - F-48: /projects/{id}/sub-projects now 301-redirects to the canonical /children URL via the existing redirects.go mechanism. Added a small redirects_test.go to lock the alias in. - F-49: dropped the meta-circular 2026-04-22 "Neuigkeiten / What's New" changelog entry that referenced "this changelog" itself. go build/vet/test clean, bun run build clean. F-25 (mobile tables → card layout) is redesign-class and is scoped at the bottom of the PR description as t-paliad-074, not implemented here. --- frontend/src/client/agenda.ts | 25 ++++++++++++++++++--- frontend/src/client/bottom-nav.ts | 12 ++++++++++ frontend/src/client/deadlines.ts | 9 +++++++- frontend/src/client/i18n.ts | 6 +++-- frontend/src/client/projects.ts | 9 +++++++- frontend/src/deadlines.tsx | 2 +- frontend/src/glossary.tsx | 8 +++---- frontend/src/projects.tsx | 2 +- frontend/src/styles/global.css | 8 +++++++ internal/changelog/changelog.go | 8 ------- internal/handlers/redirects.go | 12 ++++++++++ internal/handlers/redirects_test.go | 34 +++++++++++++++++++++++++++++ 12 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 internal/handlers/redirects_test.go diff --git a/frontend/src/client/agenda.ts b/frontend/src/client/agenda.ts index 0f02bd0..722716e 100644 --- a/frontend/src/client/agenda.ts +++ b/frontend/src/client/agenda.ts @@ -207,18 +207,35 @@ function groupByDay(items: AgendaItem[]): DayBucket[] { } function renderDay(bucket: DayBucket): string { + const expected = expectedUrgency(bucket.day); return `

${esc(relativeDayLabel(bucket.day))} ${esc(fullDateLabel(bucket.day))}

`; } -function renderItem(it: AgendaItem): string { +// F-32: an item's urgency tag duplicates the day-bucket heading in the +// common case (a "Heute" item under HEUTE, a "Diese Woche" item under "in 3 +// Tagen"). The tag stays only when it disagrees with the bucket — e.g. an +// "Überfällig" deadline that lands in today's bucket because of a filter +// quirk. expectedUrgency mirrors the server's bucketing rule against the +// bucket's day. +function expectedUrgency(day: Date): Urgency { + const today = startOfToday(); + const diff = Math.round((day.getTime() - today.getTime()) / 86400000); + if (diff < 0) return "overdue"; + if (diff === 0) return "today"; + if (diff === 1) return "tomorrow"; + if (diff <= 6) return "this_week"; + return "later"; +} + +function renderItem(it: AgendaItem, bucketUrgency: Urgency): string { const urgencyClass = `agenda-item-${it.urgency}`; const typeClass = `agenda-item-type-${it.type}`; const iconHTML = it.type === "deadline" ? deadlineIcon() : appointmentIcon(); @@ -230,7 +247,9 @@ function renderItem(it: AgendaItem): string { const timePart = it.type === "appointment" ? `${esc(formatAppointmentTime(it))}` : ""; - const urgencyTag = `${esc(t(`agenda.urgency.${it.urgency}`))}`; + const urgencyTag = it.urgency !== bucketUrgency + ? `${esc(t(`agenda.urgency.${it.urgency}`))}` + : ""; const locationPart = it.type === "appointment" && it.location ? `${esc(it.location)}` : ""; diff --git a/frontend/src/client/bottom-nav.ts b/frontend/src/client/bottom-nav.ts index 5545792..7e874af 100644 --- a/frontend/src/client/bottom-nav.ts +++ b/frontend/src/client/bottom-nav.ts @@ -1,4 +1,5 @@ import { toggleMobileSidebar } from "./sidebar"; +import { t } from "./i18n"; const KEYBOARD_THRESHOLD_PX = 100; const BADGE_REFRESH_MS = 60_000; @@ -99,11 +100,22 @@ function initAgendaBadge(): void { if (total <= 0) { badge!.style.display = "none"; badge!.classList.remove("bottom-nav-badge-overdue"); + badge!.removeAttribute("title"); + badge!.removeAttribute("aria-label"); return; } badge!.textContent = total > 9 ? "9+" : String(total); badge!.style.display = ""; badge!.classList.toggle("bottom-nav-badge-overdue", overdue > 0); + // F-38: the badge counts "actionable" items only — overdue + due + // today. The accessible label spells that out so the "2" never + // reads as ambiguous (e.g. "2 things this week"). + const label = t("bottomnav.badge.deadlines") + .replace("{overdue}", String(overdue)) + .replace("{today}", String(today)); + badge!.setAttribute("title", label); + badge!.setAttribute("aria-label", label); + badge!.setAttribute("aria-hidden", "false"); }) .catch(() => { // Badge is decorative; never break the page. diff --git a/frontend/src/client/deadlines.ts b/frontend/src/client/deadlines.ts index 36a3948..2c62b3a 100644 --- a/frontend/src/client/deadlines.ts +++ b/frontend/src/client/deadlines.ts @@ -209,11 +209,18 @@ function render() { ${esc(f.project_title)} ${ruleLabel} - ${esc(statusLabel)} + ${esc(statusLabel)} `; }) .join(""); + // F-23: when every visible row carries the same status, hide the column. + // The class is dropped as soon as the user widens the filter and variety + // reappears, so the header is never permanently removed from the DOM. + const statusUnique = new Set(allDeadlines.map((f) => f.status)).size; + const table = document.getElementById("deadlines-table"); + table?.classList.toggle("akten-table--hide-status", statusUnique <= 1); + tbody.querySelectorAll(".frist-row").forEach((row) => { const id = row.dataset.id!; row.addEventListener("click", (e) => { diff --git a/frontend/src/client/i18n.ts b/frontend/src/client/i18n.ts index 7c7cc05..ce00dc1 100644 --- a/frontend/src/client/i18n.ts +++ b/frontend/src/client/i18n.ts @@ -50,6 +50,7 @@ const translations: Record> = { "bottomnav.add.project": "Projekt anlegen", "bottomnav.add.project.sub": "Neues Mandat / Verfahren / Patent", "bottomnav.add.cancel": "Abbrechen", + "bottomnav.badge.deadlines": "{overdue} überfällig + {today} heute fällig", // Changelog (What's New) — t-paliad-027 "changelog.title": "Neuigkeiten — Paliad", @@ -252,8 +253,8 @@ const translations: Record> = { "glossar.subtitle": "Zweisprachiges Glossar der wichtigsten Begriffe im Patentrecht.", "glossar.search.placeholder": "Suchen...", "glossar.filter.all": "Alle", - "glossar.filter.litigation": "Litigation", - "glossar.filter.prosecution": "Prosecution", + "glossar.filter.litigation": "Streitsachen", + "glossar.filter.prosecution": "Erteilungsverfahren", "glossar.filter.general": "Allgemein", "glossar.col.de": "Deutsch", "glossar.col.en": "English", @@ -1431,6 +1432,7 @@ const translations: Record> = { "bottomnav.add.project": "New project", "bottomnav.add.project.sub": "New matter / case / patent", "bottomnav.add.cancel": "Cancel", + "bottomnav.badge.deadlines": "{overdue} overdue + {today} due today", // Changelog (What's New) — t-paliad-027 "changelog.title": "What's New — Paliad", diff --git a/frontend/src/client/projects.ts b/frontend/src/client/projects.ts index 5cd588b..8a565eb 100644 --- a/frontend/src/client/projects.ts +++ b/frontend/src/client/projects.ts @@ -148,12 +148,19 @@ function render() { ${esc(typeLabel)} ${refCell} ${clientMatterCell} - ${esc(statusLabel)} + ${esc(statusLabel)} ${fmtDate(p.updated_at)} `; }) .join(""); + // F-23: when every visible row shares the same status, hide the column to + // cut redundant noise. The toggle re-runs on every filter change, so the + // column comes back as soon as the rows mix again. + const statusUnique = new Set(filtered.map((p) => p.status)).size; + const table = document.getElementById("akten-table"); + table?.classList.toggle("akten-table--hide-status", statusUnique <= 1); + tbody.querySelectorAll(".akten-row").forEach((row) => { row.addEventListener("click", () => { const id = row.dataset.id!; diff --git a/frontend/src/deadlines.tsx b/frontend/src/deadlines.tsx index afe567b..57f7328 100644 --- a/frontend/src/deadlines.tsx +++ b/frontend/src/deadlines.tsx @@ -100,7 +100,7 @@ export function renderDeadlines(): string { Titel Projekt Regel - Status + Status diff --git a/frontend/src/glossary.tsx b/frontend/src/glossary.tsx index ba25d2b..e529e4d 100644 --- a/frontend/src/glossary.tsx +++ b/frontend/src/glossary.tsx @@ -57,8 +57,8 @@ export function renderGlossary(): string {
- - + + @@ -109,8 +109,8 @@ export function renderGlossary(): string {