m/paliad#54 (t-paliad-221) — fix 92780cf added a status=upcoming option
for appointments and made it the default, but DeadlineFilterUpcoming
only narrowed deadlines. The appointment query had no matching case, so
the bucket fell through to the unfiltered path and past events leaked
into "Ab heute" / "From today".
- Drop the 'upcoming' option from STATUS_OPTIONS_APPOINTMENT — confusing
label that never delivered.
- Default appointments to the 'today' bucket (matches the dashboard
tile; sane lawyer-relevant view).
- Keep 'Alle (auch vergangene)' as the explicit opt-in at the bottom
of the list.
- Defensive backend fix: map DeadlineFilterUpcoming to start_at >= today
in bucketAppointmentWindow so any persisted ?status=upcoming bookmarks
stop leaking past events.
When a pending row was drafted by Paliadin (requester_kind='agent' on
its in-flight approval_request), surface a sparkle ✨ next to the
existing eye-pill 👀. The two glyphs are orthogonal: 👀 = "needs
approval", ✨ = "Paliadin drafted this". Either can change without the
other, so the visual taxonomy stays decomposable for any future
autopilot mode where 👀 disappears but ✨ stays.
Read-path:
- DeadlineService.ListVisibleForUser + AppointmentService.ListVisibleForUser
LEFT JOIN paliad.approval_requests on pending_request_id and project
ar.requester_kind into the row. NULL when no request is pending.
- models.DeadlineWithProject + AppointmentWithProject grow
RequesterKind *string. The list-projection helpers
(projectDeadline / projectAppointment in event_service.go) carry it
into EventListItem.
- /api/events response now includes requester_kind on every pending
row; /api/inbox already does (Slice D extended approvalRequestViewColumns).
Render-path:
- frontend/src/client/events.ts — new AGENT_PILL_GLYPH constant ("✨"),
agentPill rendered into the title cell next to the existing
pendingPill when item.approval_status='pending' AND
item.requester_kind='agent'. EventListItem TS shape gains
`requester_kind?: "user" | "agent"`.
- frontend/src/client/agenda.ts — same pattern, agendaItem TS shape
+ agentPill rendered next to pendingPill in the headline span.
- frontend/src/client/inbox.ts — ApprovalRequestView gains
requester_kind + agent_turn_id; the meta line replaces the
requester's plain name with "Anna ✨ Paliadin" when the request was
drafted by the agent.
CSS: new .approval-pill--agent modifier in global.css using only
existing tokens (--color-bg-lime-tint / --color-surface-2 /
--color-text), mirroring the .approval-pill--icon shape so the two
glyphs sit side-by-side at the same baseline.
i18n: 3 new keys × 2 langs (approvals.agent.label /
approvals.agent.byline / approvals.agent.suggestion_pending) — total
1966 → 1969.
Build clean (frontend + go), tests green.
Refs: docs/design-paliadin-inline-2026-05-08.md §8.
The frontend toggle on /projects/{id} Fristen + Termine emitted
`&direct_only=true`, but `handleListEvents` and `handleEventsSummary`
never read the param, so EventListFilter / EventSummaryFilter went out
without DirectOnly and the backend always returned the subtree-aggregated
default (per t-paliad-139). The toggle has been silently dead since the
Fristen/Termine surfaces migrated to /api/events in t-paliad-139.
Backend-only fix, symmetric across endpoints:
- ListFilter (deadlines), AppointmentListFilter, EventListFilter,
EventSummaryFilter all gain DirectOnly bool.
- When ProjectID != nil && DirectOnly, the SQL predicate swaps from
projectDescendantPredicate("p") to a direct `<alias>.project_id = :project_id`
scope on each rail (deadline list, appointment list, deadline+appointment
bucket counts).
- Handlers parse `direct_only` via the existing parseDirectOnly helper.
- Test extends project_filter_descendants_test.go with three DirectOnly=true
assertions (events, deadlines, appointments) — each must collapse to the
one direct seed row.
DirectOnly is a no-op when ProjectID is nil or PersonalOnly is set —
PersonalOnly already nullifies ProjectID.
Verlauf is untouched: it still uses /api/projects/{id}/events, which
already wired direct_only via projects.go:512.
Commit 6 of 8. Renders the approval-pending warning pill on the two
busiest list surfaces:
- /events (deadline + appointment list): ⚠ pill next to the title +
soft-tinted row via .entity-row--pending-update modifier.
- /agenda (timeline): ⚠ pill in the headline + same row tint.
Changes:
- internal/services/event_service.go: EventListItem gains
ApprovalStatus *string; projectDeadline / projectAppointment
populate it from the embedded model.
- internal/services/deadline_service.go ListVisibleForUser: SQL adds
f.approval_status / pending_request_id / approved_by / approved_at
to the SELECT so DeadlineWithProject hydrates them.
- internal/services/appointment_service.go ListVisibleForUser: same
for appointments + completed_at.
- internal/services/agenda_service.go: AgendaItem gains
ApprovalStatus; the per-source SQL queries select it; the
loadDeadlines / loadAppointments projection sets it.
- frontend/src/client/events.ts renderRow: adds entity-row--pending-update
modifier and an inline approval-pill on the title cell when status='pending'.
- frontend/src/client/agenda.ts renderItem: same treatment on the
agenda-item headline.
Generic "pending update" label (approvals.pending_update.label) — not
lifecycle-specific. The inbox carries the lifecycle detail. Showing
just one pill keeps the visual signal clear; an approver scanning a
list of pending entities sees them at a glance via the row tint, then
clicks through to /inbox to see what's pending and act.
Detail pages (/deadlines/{id}, /appointments/{id}) and /dashboard
deadline rail — pill rendering for those surfaces deferred to a
follow-up to keep this commit focused. Rendered everywhere it
matters most for daily use.
Redefines the "Nur persönliche" filter on /events from "appointment with
NULL project_id" to "items where created_by = me", applied uniformly to
deadlines and appointments.
Before: client-side filter dropped every deadline row because the type
guard was `x.type === "appointment"`. m saw zero deadlines under "Nur
persönliche" even though he created plenty.
After:
- /api/events?personal_only=true (and /api/events/summary?personal_only=true)
narrow BOTH rails to f.created_by / t.created_by = current user.
ProjectID is ignored when personal_only is set (the two are
contradictory).
- DeadlineService.ListFilter and AppointmentService.AppointmentListFilter
gain CreatedBy *uuid.UUID — composes with existing visibility (AND), so
a row created on a team the user has since left still won't leak.
- Frontend drops the client-side filter; sends personal_only=true when
projectFilter === PERSONAL. URL ?personal_only=true also accepted on
initial load (bookmark-friendly alias for ?project_id=__personal__).
Personal option now shows for type=Fristen too — applies uniformly.
- 3 new live subtests covering personal_only across type=deadline /
appointment / all, with mixed-creator + multi-project + null-project
fixtures.
Selecting a Client in the project filter now returns rows attached to
that Client AND every Litigation / Patent / Case below it (and so on
down the tree). Previously the filter was exact-match: picking a Client
hid every item in the subtree, which was the opposite of what users
expect when they pick a parent in a hierarchical picker.
The descendant set comes from paliad.projects.path - every project's
path always contains its own id and every ancestor's id, so any project
whose path includes the filter UUID is either that project or a
descendant. Pattern matches the existing visibility predicate (which
walks the path UPWARD for inheritance); the new helper just inverts the
direction.
Filter sites updated:
- DeadlineService.ListVisibleForUser (/deadlines, /events)
- DeadlineService.SummaryCounts (deadline summary cards)
- AppointmentService.ListVisibleForUser (/appointments, /events)
- EventService.deadlineBuckets (/events deadline rail)
- EventService.appointmentBuckets (/events appointment rail)
ListForProject (deadline/appointment/checklist/note) is unchanged - it
fetches items for ONE specific project on the project detail page, not
a filter.
Visibility predicate (paliad.can_see_project) untouched - that walks
upward and is a different concern.
PR-2 of the Fristen+Termine unification. Pure additive change — the existing
deadlines.tsx + appointments.tsx pages stay live; this PR introduces the new
events.tsx shell + client/events.ts runtime that PR-3 will mount onto the
two routes.
Frontend (new):
- frontend/src/events.tsx — shared shell with the 3-chip type toggle
(Fristen / Termine / Beides), the 5-card summary row (Überfällig
conditional + 4 universal cards), the union filter row, and the unified
table that renders a discriminated row per type. Two header CTAs ("Neue
Frist" + "Neuer Termin") collapse to the relevant one in single-type mode.
- frontend/src/client/events.ts — runtime. Reads window.__PALIAD_EVENTS__
(PR-3 will inject defaultType from the Go handler), derives the rest from
?type query param. Card click sets status filter; the events endpoint
takes care of bucket-aware appointment-side date windowing so both rails
stay in sync in Beides mode. Hide-on-uniform pattern applied per column
(rule, event_type, location, appointment_type, status, row-type chip).
- frontend/build.ts — emits events-deadlines.html + events-appointments.html
from one renderEvents(currentPath) so each output gets the right Sidebar
highlight; client/events.ts bundle added.
- 16 i18n keys (DE+EN): events.toggle.*, events.summary.later,
events.col.*, events.row.type.*, events.empty.*, events.unavailable plus
the new deadlines.summary.later / deadlines.filter.later pair for the
Später bucket.
- CSS: --bucket-later (#1d4ed8 light / #60a5fa dark) for the Später card,
matching events-table--hide-* column hiders, .events-row-type-chip
styling, .event-type-chip-row spacing.
Backend tweaks (small):
- DeadlineFilterLater (`later`): pending deadlines past Mon-week-after.
Click-target for the Später card.
- EventService.ListVisibleForUser now derives an appointment-side date
window from a bucket-style status (today/this_week/next_week/later) so
card clicks filter both rails consistently. Overdue/Completed exclude
appointments entirely (no appointment analogue).
- pickLater / pickEarlier helpers intersect the bucket-derived window with
any caller-supplied from/to.
go build/vet/test ./... clean. bun run build clean (1394 keys, IIFE prologue
guard passes).
PR-1 of the Fristen+Termine unification (t-paliad-110). Backend layer
only — no frontend changes; the existing /deadlines and /appointments
pages still render the type-specific UIs.
EventService delegates to DeadlineService + AppointmentService for the
actual reads (no duplicate visibility logic, no duplicate event_type
hydration), then projects both into the discriminated EventListItem
union and merges/sorts by event_date asc. The handler exposes:
GET /api/events?type=deadline|appointment|all&status=…&project_id=…
&event_type=…&type_filter=…&from=…&to=…
GET /api/events/summary?type=…&project_id=…
Bucket model (per t-paliad-110 spec, supersedes t-106):
- four universal cards: Heute · Diese Woche · Nächste Woche · Später
- Überfällig is deadline-only, conditional, alarm-styled when > 0
- Erledigt drops from the card row; stays available as a filter option
- appointments have no completed_at — past appointments aren't bucketed
The deadline-side cutoffs reuse computeDeadlineBucketBounds so
/api/events/summary and /api/deadlines/summary can never disagree.
Existing /api/deadlines and /api/appointments stay untouched —
calendars, project-detail panes, and CalDAV consumers still call them
directly.