package web import ( "net/http" "strings" ) // Phase 5j Slice C — system views. Per m's Q1 pick (b) (2026-05-29): // FULL MIGRATION of the legacy pages into the /views/{slug} family. // /, /dashboard, /calendar, /timeline, /graph all 301-redirect to their // /views/{system-slug} counterparts; the handlers stay (now reachable // under the new URL). // // System views are code-resident — they never appear as rows in // projax.views. Their slugs are reserved at the validator level (see // store.IsReservedViewSlug) so user-created views can't shadow them. // SystemView is a code-resident view definition. The sidebar's Views // section (slice E) lists every entry returned by AllSystemViews // alongside user views. The render path for system slugs goes directly // to the legacy handler (handleTree / handleDashboard / …); the struct // here is metadata for navigation, not a render spec. type SystemView struct { Slug string Name string Icon string URL string // /views/{slug} } // AllSystemViews returns every code-resident view in display order. Used // by the sidebar (slice E) and the reserved-slug validation (slice A // already pre-seeded the same slugs in store.IsReservedViewSlug — keep // in sync with this list). func AllSystemViews() []SystemView { return []SystemView{ {Slug: "tree", Name: "Tree", Icon: "tree", URL: "/views/tree"}, {Slug: "dashboard", Name: "Dashboard", Icon: "dashboard", URL: "/views/dashboard"}, {Slug: "calendar", Name: "Calendar", Icon: "calendar", URL: "/views/calendar"}, {Slug: "timeline", Name: "Timeline", Icon: "clock", URL: "/views/timeline"}, {Slug: "graph", Name: "Graph", Icon: "graph", URL: "/views/graph"}, } } // LookupSystemView returns the SystemView matching slug, or nil. Used by // handleViewRender's fallback path and by tests that need to assert // metadata. func LookupSystemView(slug string) *SystemView { for _, sv := range AllSystemViews() { if sv.Slug == slug { s := sv return &s } } return nil } // legacyRedirect returns a handler that 301s the legacy URL onto its // /views/{system-slug} counterpart. Per m's Q3 pick (b): when the // request carries a legacy `?view=` param (the 5i overlay scheme) // the redirect resolves the uuid → current slug so old bookmarks land // on the user view they pointed at. A miss falls through to the system // slug. func (s *Server) legacyRedirect(systemSlug string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // / is a path-prefix in Go's mux; only redirect when the request // path is exactly "/". Any other root-relative path that fell // through to GET / (e.g. "/some-unknown") gets a 404. if systemSlug == "tree" && r.URL.Path != "/" { http.NotFound(w, r) return } target := "/views/" + systemSlug if id := strings.TrimSpace(r.URL.Query().Get("view")); id != "" { if v, err := s.Store.GetViewByID(r.Context(), id); err == nil && v != nil { target = "/views/" + v.Slug } } // Preserve any non-`view` query params so existing bookmarks // carrying ?tag=… etc. still narrow the redirected view. q := r.URL.Query() q.Del("view") if encoded := q.Encode(); encoded != "" { target += "?" + encoded } http.Redirect(w, r, target, http.StatusMovedPermanently) } }