feat(phase 3e dashboard): cross-project /dashboard with tasks, issues, recent docs
- store.RecentDocuments(since, limit) returns dated item_links + parent item - web/dashboard.go handler aggregates VTODOs + Gitea issues + dated links across every linked item, fanout via 4-worker goroutine pool, 60s TTL cache keyed by encoded TreeFilter - Tasks card: bucketed Overdue/Today/Tomorrow/Week/NoDue, sort by bucket then due asc; ✓ button completes via existing PutTodo path + busts cache - Issues card: read-only, reuses GiteaDeps.Cache - Recent docs card: last-30d event_date links, canonical PER rendered - Filter chips on top reuse tree_filter URL params (tag/mgmt/has) - nav adds "dashboard" link; design.md §"Dashboard" documents the surface - 4 integration tests (empty render, dated-link surfacing, tag filter, cache hit)
This commit is contained in:
@@ -24,13 +24,14 @@ var staticFS embed.FS
|
||||
|
||||
// Server bundles handlers, templates, and the store.
|
||||
type Server struct {
|
||||
Store *store.Store
|
||||
pages map[string]*template.Template
|
||||
Logger *slog.Logger
|
||||
Auth *AuthConfig // nil → no auth (local dev / tests)
|
||||
CalDAV *CalDAVDeps // nil → CalDAV integration disabled
|
||||
Gitea *GiteaDeps // nil → Gitea integration disabled
|
||||
MCP http.Handler // nil → /mcp/ routes return 404 (off cleanly)
|
||||
Store *store.Store
|
||||
pages map[string]*template.Template
|
||||
Logger *slog.Logger
|
||||
Auth *AuthConfig // nil → no auth (local dev / tests)
|
||||
CalDAV *CalDAVDeps // nil → CalDAV integration disabled
|
||||
Gitea *GiteaDeps // nil → Gitea integration disabled
|
||||
MCP http.Handler // nil → /mcp/ routes return 404 (off cleanly)
|
||||
dashboard *dashboardCache
|
||||
}
|
||||
|
||||
// New builds a Server. Each page is parsed alongside the layout into its own
|
||||
@@ -134,6 +135,22 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
|
||||
}
|
||||
pages["login"] = loginTmpl
|
||||
|
||||
// Dashboard page + its section fragment.
|
||||
dashTmpl, err := template.New("dashboard").Funcs(funcs).ParseFS(templatesFS,
|
||||
"templates/layout.tmpl",
|
||||
"templates/dashboard.tmpl",
|
||||
"templates/dashboard_section.tmpl",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse dashboard: %w", err)
|
||||
}
|
||||
pages["dashboard"] = dashTmpl
|
||||
dashSection, err := template.New("dashboard_section").Funcs(funcs).ParseFS(templatesFS, "templates/dashboard_section.tmpl")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse dashboard_section: %w", err)
|
||||
}
|
||||
pages["dashboard_section"] = dashSection
|
||||
|
||||
// Bulk-edit page + its fragment + per-row chip cells. The chip cells share
|
||||
// definitions with bulk_section so we parse them together every time.
|
||||
bulkTmpl, err := template.New("bulk").Funcs(funcs).ParseFS(templatesFS,
|
||||
@@ -161,7 +178,12 @@ func New(s *store.Store, logger *slog.Logger) (*Server, error) {
|
||||
}
|
||||
pages["bulk_chip_mgmt"] = bulkChipMgmt
|
||||
|
||||
return &Server{Store: s, pages: pages, Logger: logger}, nil
|
||||
return &Server{
|
||||
Store: s,
|
||||
pages: pages,
|
||||
Logger: logger,
|
||||
dashboard: newDashboardCache(60 * time.Second),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Routes wires every URL to a handler and returns the mux.
|
||||
@@ -174,6 +196,8 @@ func (s *Server) Routes() http.Handler {
|
||||
mux.HandleFunc("GET /new", s.handleNewForm)
|
||||
mux.HandleFunc("POST /new", s.handleNewSubmit)
|
||||
mux.HandleFunc("GET /admin/classify", s.handleClassify)
|
||||
mux.HandleFunc("GET /dashboard", s.handleDashboard)
|
||||
mux.HandleFunc("POST /dashboard/task/done", s.handleDashboardTaskDone)
|
||||
mux.HandleFunc("GET /admin/bulk", s.handleBulk)
|
||||
mux.HandleFunc("POST /admin/bulk/apply", s.handleBulkApply)
|
||||
mux.HandleFunc("POST /admin/bulk/chip", s.handleBulkChip)
|
||||
@@ -602,6 +626,8 @@ func (s *Server) render(w http.ResponseWriter, name string, data map[string]any)
|
||||
entry = "documents-section"
|
||||
case "bulk_section":
|
||||
entry = "bulk-section"
|
||||
case "dashboard_section":
|
||||
entry = "dashboard-section"
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := t.ExecuteTemplate(w, entry, data); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user