design(t-paliad-144): m signed off + Q4 correction (3 shapes, not 4)

m's lock-in 2026-05-07: agree with all recommendations on Q1-Q18 and §10
Q19-Q27, with one correction on Q4: "activity" is a content selection
(sources + filters), not a render shape. Folded into `list` shape with
density: "compact" + actor/time columns. Shape ⊥ source — any source can
render in any shape.

Render shapes for v1: list / cards / calendar (3, was 4).

PR split decision (delegated to inventor): A1 backend substrate + API
(no UI change, ~1800 LoC, smoke via curl) → main → A2 frontend Custom
Views UI (~1600 LoC, additive on A1) → main.

Status flipped DRAFT → LOCKED. Inventor → coder transition initiated.
This commit is contained in:
m
2026-05-07 12:36:05 +02:00
parent 5c263102e3
commit 956ff10e4d

View File

@@ -4,7 +4,7 @@
**Issue:** m/paliad#5
**Author:** noether (inventor)
**Date:** 2026-05-06
**Status:** DRAFT — awaiting m's go/no-go on §10 open questions
**Status:** LOCKED 2026-05-07 — m signed off on all recommendations + §10 follow-ups, with one correction (Q4 narrowed from 4 shapes → 3; "activity" is a filter/source choice, not a render shape — folded into `list` shape with density config). Inventor → coder transition initiated. PR split chosen: A1 backend substrate, A2 frontend Custom Views.
**Branch:** `mai/noether/inventor-data-display`
**Builds on:** t-paliad-109 (events unification, shipped) + t-paliad-138 (approvals, shipped) + t-paliad-139 (hierarchy aggregation, all 3 phases on `mai/noether/inventor-project` awaiting merge gate)
@@ -58,7 +58,7 @@ Three design pieces fall out of this:
|---|---|---|
| **Substrate shape** | One `ViewService` (new) that union-loads from 4 data sources: `deadline`, `appointment`, `project_event` (audit), `approval_request`. Returns a discriminated `[]ViewRow` keyed by `kind`. | Single virtual SQL `view_row` table with UNION ALL across all 4 — too many polymorphic columns; harder to evolve per-source filters. |
| **Filter grammar** | Structured JSON spec validated server-side (`FilterSpec`). UI builds it via affordance widgets; the JSON is also human-editable for power users. | SQL DSL (security risk + complexity); UI-only (forces every dimension to have a widget). |
| **Render shapes for v1** | `list`, `cards`, `calendar`, `activity` (4). Defer `kanban`, `connections-graph`, `timeline-distinct-from-cards`. | Ship all 6 — `/events` already has list/cards/calendar; `cards` doubles as the day-grouped timeline today. Activity is the unified-inbox shape. The deferred three each need substantial new component work. |
| **Render shapes for v1** | `list`, `cards`, `calendar` (3). Activity-feed appearance is achieved by source/filter choice (`sources: ["project_event", …]`) rendered through `list` shape with `density: "compact"` + actor/time columns — *not* a separate shape. Defer `kanban`, `connections-graph`, `timeline-distinct-from-cards`. | Ship 4+ shapes including a dedicated "activity" — m's correction (2026-05-07): activity is content selection, not visualisation. Shape ⊥ source. |
| **Persistence** | New table `paliad.user_views` (id, user_id, slug, name, filter_spec jsonb, render_spec jsonb, sort_order, icon, last_used_at, …). RLS = caller's own rows only. | Per-user JSON column on `paliad.users` — kills the sidebar count badge query path (`SELECT count(*) WHERE user_id`); also no indexed sort. |
| **System defaults — code or DB?** | **Code.** Defaults stay as their own pages (`/dashboard`, `/agenda`, `/events`, `/inbox`); they are *built using the same render components* the custom-view system uses. No `is_system=true` row in `user_views`. | Seed system rows per user — drifts on schema bumps; new users miss bumps; `is_system=true` is a synonym for "config-as-data when config-as-code is cleaner". |
| **Sidebar** | New "Meine Sichten" group between "Arbeit" and "Werkzeuge". Each saved view appears as one nav entry (icon + name). One trailing "+ Neue Sicht" entry. | "Meine Sichten" as a single sidebar entry expanding to a panel — extra click cost on every navigation. |
@@ -243,18 +243,25 @@ These are genuinely two views; the substrate exposes both.
### Q4 — Which render shapes are first-class for v1?
**Recommendation: `list`, `cards`, `calendar`, `activity` — four shapes.**
**Recommendation: `list`, `cards`, `calendar` — three shapes.**
m's correction (2026-05-07): activity is a content selection (sources + filters), not a render shape. The "compact one-line stream with type icons" appearance is `list` shape with `density: "compact"` + an actor/time column set — same component, different config. Shape is orthogonal to source: any source can render in any shape.
| Shape | Status today | What it does | Source bias |
|---|---|---|---|
| **`list`** | shipped on `/events` (table) and `/inbox` (`<ul class="inbox-list">`) | One row per result; columns vary per source. Table for desktop, stacked card-rows on mobile. | source-agnostic |
| **`cards`** | shipped on `/agenda` (day-grouped timeline) and dashboard activity feed | Day-grouped chronological cards; primary date drives grouping. Already the unified-inbox-feel today. | source-agnostic |
| **`list`** | shipped on `/events` (table), `/inbox` (`<ul class="inbox-list">`), `/dashboard` activity feed | One row per result; columns vary per source. Table for desktop, stacked card-rows on mobile. Density modes: `comfortable` (default, full table) / `compact` (one-line stream — the activity-feed look). | source-agnostic |
| **`cards`** | shipped on `/agenda` (day-grouped timeline) | Day-grouped chronological cards; primary date drives grouping. The unified-inbox-feel m described — *when fed activity-style content*. | source-agnostic |
| **`calendar`** | shipped on `/events?view=calendar` | Month grid (toggleable to week). Shows up to N pills per day. Click → popup with the day's rows. | works best for time-bound sources (deadline, appointment, project_event) |
| **`activity`** | partial — `dashboard-activity-list` on `/dashboard` | Compact one-line-per-row stream; icon per source kind, "X has done Y on project Z, 3h ago". The unified-inbox shape m described. | works for everything; designed for Verlauf-style consumption |
How "activity feed" is expressed in this model:
- **Filter side**: `sources: ["project_event", "approval_request"]`, `time.horizon: past_30d`, `time.field: created_at`.
- **Render side**: `shape: "list"`, `list.density: "compact"`, `list.columns: ["time", "actor", "title", "project"]`.
That same `list` shape — with `density: "comfortable"` + the deadline column set — also powers `/events`. One component, two configs. Same logic for `cards`: the day-grouped Verlauf on `/projects/{id}` and a "newest cases this week" card view share the component.
Defer to v2: `kanban` (no obvious column axis across mixed sources), `connections-graph` (the events↔files visualisation referenced in the issue body — that's specifically about graph rendering, which is a 5x bigger component and works better as its own page than as a saved-view shape), `timeline-distinct-from-cards` (a horizontal Gantt would be the natural shape but adds a lot for marginal value at v1).
Why these four and not all six: each shape is a real frontend component with empty states, error states, layout, density toggles, mobile behaviour. We have four shipped today (`list`, `cards`, `calendar`, `activity`) — generalising them costs little. Adding two more (`kanban`, `graph`) is each its own component-week. Better to ship 4 polished than 6 half-baked.
Why these three and not all six: each shape is a real frontend component with empty states, error states, layout, density toggles, mobile behaviour. We have three already shipped today, generalising them costs little. Adding `kanban` + `graph` is each its own component-week. Better to ship 3 polished than 6 half-baked.
### Q5 — Per-shape config
@@ -262,25 +269,24 @@ Why these four and not all six: each shape is a real frontend component with emp
```json
{
"shape": "cards",
"list": { "columns": ["date", "title", "project", "status"], "sort": "date_asc" },
"shape": "list",
"list": { "columns": ["date", "title", "project", "status"], "sort": "date_asc", "density": "comfortable" },
"cards": { "group_by": "day", "sort": "date_asc", "show_empty_days": false },
"calendar": { "default_view": "month", "show_weekends": true },
"activity": { "density": "comfortable", "show_avatars": true }
"calendar": { "default_view": "month", "show_weekends": true }
}
```
The user picks one `shape`; the matching config block is read at render time. Other shape configs are kept (so flipping back to a previously-used shape preserves its tweaks).
UI: the shape switcher is a 4-button row at the top of every view page. Right of it, a small "Shape settings" gear opens a modal with the per-shape knobs. Most users never touch the gear.
UI: the shape switcher is a **3-button row** at the top of every view page. Right of it, a small "Shape settings" gear opens a modal with the per-shape knobs. Most users never touch the gear.
Default values per shape:
- `list.columns` = source-determined (deadline view = date/title/rule/status; appointment view = date/title/location/type; activity view = time/actor/title)
- `list.columns` = source-determined (deadline view = date/title/rule/status; appointment view = date/title/location/type; activity-feel view = time/actor/title — auto-selected when sources are activity-flavoured)
- `list.density` = `"comfortable"` for entity sources, `"compact"` when sources include project_event or approval_request
- `list.sort` = `"date_asc"` for forward-looking views, `"date_desc"` for retrospective
- `cards.group_by` = `"day"`
- `calendar.default_view` = `"month"`
- `activity.density` = `"comfortable"`
### Q6 — Empty state per view
@@ -634,7 +640,7 @@ User-facing strings:
- "Neue Sicht" / "New View" (creation entry)
- "Speichern als Sicht" / "Save as View"
- "Sicht bearbeiten" / "Edit View"
- shape labels: "Liste / List", "Karten / Cards", "Kalender / Calendar", "Aktivität / Activity"
- shape labels: "Liste / List", "Karten / Cards", "Kalender / Calendar"
- per-source labels: "Fristen / Deadlines", "Termine / Appointments", "Projekt-Verlauf / Project history", "Genehmigungen / Approvals"
- empty-state composition strings (filter summary)
- error toast for inaccessible-project case
@@ -651,6 +657,17 @@ If telemetry shows mobile users routinely hitting saved views, consider a "Pin t
## 7. Section E — Implementation phasing (PR shape)
### PR split decision (2026-05-07)
m delegated the split call to the inventor. Phase A is split into **two stacked PRs**:
- **A1 — Backend substrate + Custom Views API.** Migration 056, FilterSpec/RenderSpec types + validators, ViewService 4-source extension, UserViewService CRUD, SystemView registry, all `/api/*` endpoints, full backend test coverage. *No user-visible change.* Smoke-testable via curl. ~1800 LoC.
- **A2 — Frontend Custom Views UI.** Generic view shell (`/views/{slug}`), view editor (`/views/new`, `/views/{slug}/edit`), 3 render-shape components (list/cards/calendar), sidebar "Meine Sichten" group, i18n, CSS. Builds on A1's API. ~1600 LoC.
Why split: A1 is mergeable + deployable in isolation (additive, no UI risk), exercises the validator surface, lets A2 build on a stable contract. A2 is purely additive once A1 lands. Each PR fits in a normal review window.
A1 → main → A2 → main is the merge order.
### Phase A — substrate + Custom Views (this task's locked scope)
| Step | Files | Approx. LoC | Notes |
@@ -707,9 +724,11 @@ m's brainstorm: "approval candidates + project activity + new cases + status cha
`RenderSpec`:
```json
{ "shape": "activity", "activity": { "density": "comfortable", "show_avatars": true } }
{ "shape": "list", "list": { "density": "compact", "columns": ["time", "actor", "title", "project"], "sort": "date_desc" } }
```
(The "activity-feed feel" comes from `density: "compact"` + the actor/time column set, not from a separate shape — m's correction 2026-05-07.)
User saves as `meine-aktivitaet`. URL: `/views/meine-aktivitaet`. Sidebar entry under "Meine Sichten" with the bell icon. show_count=true → badge shows count of pending approvals + new audit events in past 30d.
### 8.2 The "myVerySpecialAgenda"
@@ -741,7 +760,7 @@ User saves as `meine-aktivitaet`. URL: `/views/meine-aktivitaet`. Sidebar entry
}
```
`RenderSpec`: `{ "shape": "activity" }`
`RenderSpec`: `{ "shape": "list", "list": { "density": "compact", "columns": ["time", "actor", "title"], "sort": "date_desc" } }`
(`scope.projects` referencing a top-level Client UUID + the path-walk visibility predicate naturally pulls all descendants — this is exactly the t-139 aggregation, surfaced through the substrate.)
@@ -762,6 +781,8 @@ User saves as `meine-aktivitaet`. URL: `/views/meine-aktivitaet`. Sidebar entry
## 10. Section H — Open questions for m
**Status: LOCKED 2026-05-07.** m signed off on all Q19Q27 recommendations.
Inventor has made recommendations on every Q1Q18 from the issue body. The questions below are points where m's call would specifically refine the design before coder shift starts. Numbered fresh (Q19+) so they don't collide with the issue body's numbering.
**Q19. Curated `project_event` event-type list.**