package services // SystemView is a code-resident view definition. The four system pages // (dashboard / agenda / events / inbox) resolve to one of these when // they want to consume the substrate as if they were a Custom View. // // Design: docs/design-data-display-model-2026-05-06.md §5 Q8. // // Q8 lock-in: defaults are config-as-code, not seeded rows in // paliad.user_views. Their slugs are reserved (validator rejects // matching user-view slugs). import ( "slices" ) // SystemView is the in-process projection used by the substrate's // SystemView callers. It mirrors the persisted user-view shape but // never round-trips through the DB. type SystemView struct { Slug string // matches the system-page URL ("/dashboard" → "dashboard") Name string // display label (kept English here; UI re-translates via i18n) Filter FilterSpec // canonical filter the page resolves to today Render RenderSpec // canonical render shape } // DashboardSystemView returns the SystemView definition for /dashboard. // // Note: /dashboard is composed of multiple sections (5-bucket summary + // matter card + two-column lists + activity feed). It does NOT resolve // to a single FilterSpec/RenderSpec — Phase B will compose several // SystemView resolutions into the dashboard page. This entry exists so // the slug is known to the reserved-list and so future composition has // a stable hook. func DashboardSystemView() SystemView { return SystemView{ Slug: "dashboard", Name: "Dashboard", // Placeholder filter — the dashboard composes multiple queries // in Phase B; this single spec covers the activity feed only. Filter: FilterSpec{ Version: SpecVersion, Sources: []DataSource{SourceProjectEvent}, Scope: ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}}, Time: TimeSpec{Horizon: HorizonPast30d, Field: FieldCreatedAt}, }, Render: RenderSpec{ Shape: ShapeList, List: &ListConfig{ Density: DensityCompact, Sort: SortDateDesc, Columns: []string{"time", "actor", "title", "project"}, }, }, } } // AgendaSystemView returns the SystemView definition for /agenda — a // day-grouped feed of upcoming deadlines + appointments. func AgendaSystemView() SystemView { return SystemView{ Slug: "agenda", Name: "Agenda", Filter: FilterSpec{ Version: SpecVersion, Sources: []DataSource{SourceDeadline, SourceAppointment}, Scope: ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}}, Time: TimeSpec{Horizon: HorizonNext30d, Field: FieldAuto}, Predicates: map[DataSource]Predicates{ SourceDeadline: {Deadline: &DeadlinePredicates{Status: []string{"pending"}}}, }, }, Render: RenderSpec{ Shape: ShapeCards, Cards: &CardsConfig{GroupBy: CardsGroupByDay, Sort: SortDateAsc}, }, } } // EventsSystemView returns the SystemView definition for /events — the // table view over deadlines + appointments. The legacy URL keeps a // per-type chip toggle; this SystemView reflects the "all" tab default. func EventsSystemView() SystemView { return SystemView{ Slug: "events", Name: "Events", Filter: FilterSpec{ Version: SpecVersion, Sources: []DataSource{SourceDeadline, SourceAppointment}, Scope: ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}}, Time: TimeSpec{Horizon: HorizonAny, Field: FieldAuto}, }, Render: RenderSpec{ Shape: ShapeList, List: &ListConfig{ Density: DensityComfortable, Sort: SortDateAsc, }, }, } } // InboxSystemView returns the SystemView definition for /inbox. // // t-paliad-249 (Slice A, 2026-05-25) widened the inbox from // approval-requests-only to a project-events feed PLUS approval // requests. Sources is [ApprovalRequest, ProjectEvent]; the project // rail is narrowed to InboxProjectEventKinds (curated set, head pick // Q1=A). The `*_approval_*` audit events are de-duplicated against // the approval_request rows by view_service.allowedProjectEventKinds. // // Time window defaults to last 30 days; the bar's time-axis chip // can widen or narrow. Sort is newest-first — different from the // pre-249 ascending default; m's inbox metaphor is "what just // happened", not "what's coming up". // // RowAction = RowActionInbox → shape-list.ts dispatches per // row.kind: approval rows get the approve/reject/revoke layout, // project_event rows get a navigate-style stream row. func InboxSystemView() SystemView { return SystemView{ Slug: "inbox", Name: "Inbox", Filter: FilterSpec{ Version: SpecVersion, Sources: []DataSource{SourceApprovalRequest, SourceProjectEvent}, Scope: ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}}, Time: TimeSpec{Horizon: HorizonPast30d, Field: FieldAuto}, Predicates: map[DataSource]Predicates{ SourceApprovalRequest: {ApprovalRequest: &ApprovalRequestPredicates{ ViewerRole: "any_visible", Status: []string{"pending"}, }}, SourceProjectEvent: {ProjectEvent: &ProjectEventPredicates{ EventTypes: InboxProjectEventKinds, }}, }, }, Render: RenderSpec{ Shape: ShapeList, List: &ListConfig{ Density: DensityComfortable, Sort: SortDateDesc, RowAction: RowActionInbox, }, }, } } // InboxRequesterSystemView is the "Eigene Anfragen" sibling view of // /inbox. Reachable via the bar's approval_viewer_role chip ("Eigene // Anfragen") on the /inbox surface, or as its own URL on /views/inbox-mine. func InboxRequesterSystemView() SystemView { return SystemView{ Slug: "inbox-mine", Name: "Inbox (mine)", Filter: FilterSpec{ Version: SpecVersion, Sources: []DataSource{SourceApprovalRequest}, Scope: ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}}, Time: TimeSpec{Horizon: HorizonAny, Field: FieldAuto}, Predicates: map[DataSource]Predicates{ SourceApprovalRequest: {ApprovalRequest: &ApprovalRequestPredicates{ ViewerRole: "self_requested", }}, }, }, Render: RenderSpec{ Shape: ShapeList, List: &ListConfig{ Density: DensityComfortable, Sort: SortDateAsc, RowAction: RowActionApprove, }, }, } } // AllSystemViews returns every system-defined view in registration order. // Used by the reserved-slug list and by future Phase B composition. func AllSystemViews() []SystemView { return []SystemView{ DashboardSystemView(), AgendaSystemView(), EventsSystemView(), InboxSystemView(), InboxRequesterSystemView(), } } // reservedUserViewSlugs is the static list of slugs the user-view CRUD // rejects on create / update. Includes the SystemView slugs plus URLs // the application owns at the top level (admin, settings, login, …). // // Q23 lock-in (m, 2026-05-07): list as drafted. var reservedUserViewSlugs = []string{ // SystemView slugs: "dashboard", "agenda", "events", "inbox", "inbox-mine", // /views/* routes: "new", "edit", // Top-level application URLs: "tools", "admin", "settings", "login", "logout", "projects", "team", "courts", "glossary", "links", "downloads", "checklists", "views", "changelog", } // IsReservedUserViewSlug returns true if `slug` matches a reserved slug. // User-view CRUD rejects matches with ErrInvalidInput. Case-folded so // "Dashboard" is also rejected. func IsReservedUserViewSlug(slug string) bool { return slices.Contains(reservedUserViewSlugs, foldSlug(slug)) } // foldSlug normalises a slug for reserved-list comparison. Slugs are // already lowercased + dash-only by the validator before this is called, // but this lets IsReservedUserViewSlug be safe under direct calls. func foldSlug(s string) string { out := make([]byte, 0, len(s)) for i := 0; i < len(s); i++ { c := s[i] switch { case c >= 'A' && c <= 'Z': out = append(out, c+('a'-'A')) default: out = append(out, c) } } return string(out) }