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:
@@ -450,6 +450,61 @@ func (s *Store) AddLinkDated(ctx context.Context, itemID, refType, refID, rel st
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
// RecentDocuments returns every dated item_link across the whole schema with
|
||||
// event_date in [since, now], newest-first. Used by the /dashboard "Recent
|
||||
// documents" card. Soft-deleted items are excluded — 0013's cascade trigger
|
||||
// removes their links, so the join against projax.items is technically
|
||||
// redundant but kept defensively to match the items_unified guarantee.
|
||||
func (s *Store) RecentDocuments(ctx context.Context, since time.Time, limit int) ([]*ItemLinkWithItem, error) {
|
||||
if limit <= 0 {
|
||||
limit = 30
|
||||
}
|
||||
rows, err := s.Pool.Query(ctx, `
|
||||
select l.id, l.item_id, l.ref_type, l.ref_id, l.rel, l.note, l.metadata, l.created_at, l.event_date,
|
||||
i.slug, i.title, i.paths
|
||||
from projax.item_links l
|
||||
join projax.items i on i.id = l.item_id and i.deleted_at is null
|
||||
where l.event_date is not null
|
||||
and l.event_date >= $1
|
||||
order by l.event_date desc, l.created_at desc
|
||||
limit $2`, since, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []*ItemLinkWithItem{}
|
||||
for rows.Next() {
|
||||
var x ItemLinkWithItem
|
||||
if err := rows.Scan(
|
||||
&x.Link.ID, &x.Link.ItemID, &x.Link.RefType, &x.Link.RefID, &x.Link.Rel,
|
||||
&x.Link.Note, &x.Link.Metadata, &x.Link.CreatedAt, &x.Link.EventDate,
|
||||
&x.ItemSlug, &x.ItemTitle, &x.ItemPaths,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, &x)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ItemLinkWithItem bundles an item_link with a thin slice of its parent
|
||||
// item's fields — enough for the dashboard "Recent documents" row to render
|
||||
// without a second store hop.
|
||||
type ItemLinkWithItem struct {
|
||||
Link ItemLink
|
||||
ItemSlug string
|
||||
ItemTitle string
|
||||
ItemPaths []string
|
||||
}
|
||||
|
||||
// PrimaryPath returns the first path of the bundled item, mirroring Item.PrimaryPath.
|
||||
func (x *ItemLinkWithItem) PrimaryPath() string {
|
||||
if len(x.ItemPaths) == 0 {
|
||||
return ""
|
||||
}
|
||||
return x.ItemPaths[0]
|
||||
}
|
||||
|
||||
// DatedLinks returns every item_link with an event_date set, ordered
|
||||
// newest-first then by insertion order. Used by the detail-page Documents
|
||||
// section.
|
||||
|
||||
Reference in New Issue
Block a user