-- t-paliad-144 Phase A1: Custom Views — paliad.user_views. -- -- Design: docs/design-data-display-model-2026-05-06.md (noether, -- m-locked 2026-05-07). -- -- Stores per-user saved view definitions. A view is a `(filter_spec, -- render_spec, sidebar metadata)` tuple. RLS scopes every operation -- to the calling user — there is no cross-user visibility in v1. -- -- System defaults (dashboard / agenda / events / inbox) stay code- -- resident in internal/services/system_views.go. They never appear -- as rows in this table; the slugs are reserved and rejected at write -- time by the application layer. -- -- Sections: -- 1. CREATE paliad.user_views (with RLS). -- 2. Indexes. -- ============================================================================ -- 1. paliad.user_views -- ============================================================================ CREATE TABLE paliad.user_views ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES paliad.users(id) ON DELETE CASCADE, -- Stable user-facing identifier. Goes into the URL. -- Application-layer validator enforces ^[a-z0-9][a-z0-9-]{0,62}$ + -- a reserved-list rejection (dashboard, agenda, events, inbox, …). slug text NOT NULL, -- Display name. Free-form; user picks the language they think in. -- Rendered verbatim in the sidebar; no fallback or translation. name text NOT NULL, -- One of a fixed set of icon keys (see Sidebar.tsx icon registry). -- NULL → default icon (folder). Validator caps length to keep the -- column sane even if the registry is bypassed. icon text, -- Filter spec — see internal/services/filter_spec.go FilterSpec. -- Validated on write; jsonb here for forward-compat without -- migrations as new dimensions land. filter_spec jsonb NOT NULL, -- Render spec — see internal/services/render_spec.go RenderSpec. render_spec jsonb NOT NULL, -- Sidebar ordering. Lower-first. New views land at MAX+1 server-side -- so they sort to the bottom; the editor lets users drag-reorder. sort_order integer NOT NULL DEFAULT 0, -- Show a row-count badge on the sidebar entry. Costs one COUNT(*) -- per refresh; opt-in (default false) so casual users don't pay. show_count boolean NOT NULL DEFAULT false, -- Most-recently-used landing on /views (Q10). Updated by a fire- -- and-forget PATCH on every view-load. last_used_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), UNIQUE (user_id, slug) ); CREATE INDEX user_views_owner_idx ON paliad.user_views (user_id, sort_order); ALTER TABLE paliad.user_views ENABLE ROW LEVEL SECURITY; -- Owner-only access. No global_admin override: views are personal -- working state, not auditable infrastructure. CREATE POLICY user_views_owner_all ON paliad.user_views FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());