Files
projax/web/templates/dashboard_section.tmpl
mAi 7ed0a4d46c feat(phase 4a): chronological timeline at /timeline + dashboard VTODO edit/delete
/timeline braids every dated thing in projax into a single chronological spine:
CalDAV VTODOs (DUE anchor), VEVENTs (DTSTART), dated item_links (event_date),
and item-creation markers. Default window past-30d to future-90d; ?order=
toggles asc/desc; ?kind= narrows by row type; tree filter (?tag/?mgmt/?has)
applies across kinds. Today / Tomorrow get sticky pills; rows > today+30d
fade. 90s in-memory TTL cache keyed by (filter, window, order, kinds);
busted on any VTODO writeback or dated-link change.

Scope expansion (per head message during 4a): the dashboard Tasks card now
has edit + delete affordances on every row, matching the detail page. New
/dashboard/task/{edit,delete} endpoints share a writeback path with /done.
Timeline VTODO rows reuse the same handlers; HX-Target=timeline-section
selects the re-render surface. Timeline item_link rows reuse the existing
/i/{path}/links/remove handler with the same surface-switch.

VEVENT rows on the timeline remain read-only at v1 (3l decision stands).
Item-creation events render as muted "added X to projax" markers.

Tests cover empty state, dated-doc surfacing, kind-filter narrowing, order
toggle, mixed CalDAV todos + all-day events (with the (2 days) duration
hint), and tag-filter cross-kind. New dashboard test asserts the edit/
delete affordances are wired up.

docs/design.md gains §12 with the full source list, layout rules, time
window, filter integration, cache TTL, and deferred items.
2026-05-16 15:52:32 +02:00

216 lines
9.2 KiB
Cheetah
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{{define "dashboard-section"}}
<section id="dashboard-section" class="dashboard">
<section class="tagbar" id="dashboard-filterbar">
<form id="dashboard-filter" class="search"
hx-get="/dashboard"
hx-target="#dashboard-section"
hx-swap="outerHTML"
hx-trigger="change from:select"
hx-push-url="true">
<label>tag&nbsp;
<select name="tag" multiple size="3">
{{$sel := .Filter.Tags}}
{{range $.Filter.Tags}}<option value="{{.}}" selected>{{.}}</option>{{end}}
</select>
</label>
<label>mgmt&nbsp;
<select name="mgmt" multiple size="4">
{{$selM := .Filter.Management}}
<option value="mai" {{if contains $selM "mai"}}selected{{end}}>mai</option>
<option value="self" {{if contains $selM "self"}}selected{{end}}>self</option>
<option value="external" {{if contains $selM "external"}}selected{{end}}>external</option>
</select>
</label>
<label>has&nbsp;
<select name="has" multiple size="2">
{{$selH := .Filter.HasLinks}}
<option value="caldav-list" {{if contains $selH "caldav-list"}}selected{{end}}>caldav</option>
<option value="gitea-repo" {{if contains $selH "gitea-repo"}}selected{{end}}>gitea</option>
</select>
</label>
{{if .Filter.Active}}<a class="clear" href="/dashboard">clear filters</a>{{end}}
</form>
<p class="counts muted">
{{if .P.Cached}}<small title="Served from 60s in-memory cache · built {{.P.BuiltAt.Format "15:04:05"}}">updated {{.UpdatedRel}} · cached</small>
{{else}}<small>updated {{.UpdatedRel}} · fresh</small>{{end}}
<a class="refresh" href="{{.RefreshURL}}"
hx-get="{{.RefreshURL}}"
hx-target="#dashboard-section"
hx-swap="outerHTML"
title="force-refresh: bust the 60s cache for this filter">↻ refresh</a>
</p>
</section>
<section class="dash-grid">
{{$collapse := not .FilterActive}}
{{if or .P.Tasks (not $collapse)}}
<article class="card card-tasks">
<header>
<h2>Open tasks <small class="muted">({{.P.TaskTotal}})</small></h2>
{{if or .P.TaskGroups.Overdue .P.TaskGroups.Today .P.TaskGroups.Tomorrow .P.TaskGroups.Week .P.TaskGroups.NoDue}}
<p class="task-groups muted">
{{if .P.TaskGroups.Overdue}}<span class="overdue">Overdue ({{.P.TaskGroups.Overdue}})</span>{{end}}
{{if .P.TaskGroups.Today}}<span>Today ({{.P.TaskGroups.Today}})</span>{{end}}
{{if .P.TaskGroups.Tomorrow}}<span>Tomorrow ({{.P.TaskGroups.Tomorrow}})</span>{{end}}
{{if .P.TaskGroups.Week}}<span>This week ({{.P.TaskGroups.Week}})</span>{{end}}
{{if .P.TaskGroups.NoDue}}<span>No due ({{.P.TaskGroups.NoDue}})</span>{{end}}
</p>
{{end}}
</header>
{{if .P.Tasks}}
<ul class="task-list">
{{range .P.Tasks}}
{{$cal := .CalendarURL}}
{{$uid := .Todo.UID}}
{{$path := .Item.PrimaryPath}}
<li class="task-row bucket-{{.Bucket}}" id="task-{{$uid}}" data-item-path="{{$path}}" data-vtodo-uid="{{$uid}}">
<form class="check"
hx-post="/dashboard/task/done"
hx-target="#dashboard-section"
hx-swap="outerHTML">
<input type="hidden" name="calendar_url" value="{{$cal}}">
<input type="hidden" name="uid" value="{{$uid}}">
<button type="submit" title="mark complete">✓</button>
</form>
<a class="proj" href="/i/{{$path}}">{{$path}}</a>
<span class="summary">{{.Todo.Summary}}</span>
{{if .DueRel}}<span class="due {{if eq .Bucket "overdue"}}bad{{end}}" title="{{if .Todo.Due}}{{.Todo.Due.Format "2006-01-02"}}{{end}}">{{.DueRel}}</span>{{end}}
<details class="task-edit">
<summary class="muted" title="edit summary / due">✎</summary>
<form class="todo-edit inline"
hx-post="/dashboard/task/edit"
hx-target="#dashboard-section"
hx-swap="outerHTML">
<input type="hidden" name="calendar_url" value="{{$cal}}">
<input type="hidden" name="uid" value="{{$uid}}">
<input type="text" name="summary" value="{{.Todo.Summary}}" required>
<input type="date" name="due" value="{{if .Todo.Due}}{{.Todo.Due.Format "2006-01-02"}}{{end}}">
<button type="submit">Save</button>
</form>
</details>
<form class="todo-delete inline"
hx-post="/dashboard/task/delete"
hx-target="#dashboard-section"
hx-swap="outerHTML"
hx-confirm="Delete this task? This cannot be undone.">
<input type="hidden" name="calendar_url" value="{{$cal}}">
<input type="hidden" name="uid" value="{{$uid}}">
<button type="submit" class="x" title="Delete" aria-label="Delete">×</button>
</form>
</li>
{{end}}
</ul>
{{else}}
<p class="empty muted">Nothing open. Nice.</p>
{{end}}
</article>
{{else}}
<p class="card-collapsed muted">No open tasks.</p>
{{end}}
{{if or .P.Events (not $collapse)}}
<article class="card card-events">
<header>
<h2>Events <small class="muted">({{.P.EventsTotal}} upcoming, next 7d)</small></h2>
</header>
{{if .P.Events}}
{{range .P.Events}}
<div class="event-day">
<h3 class="muted">{{.DayLabel}} <small>({{len .Events}})</small></h3>
<ul class="event-list">
{{range .Events}}
<li class="event-row">
<span class="start">{{.StartLabel}}</span>
<a class="proj" href="/i/{{.Item.PrimaryPath}}">{{.Item.PrimaryPath}}</a>
<span class="summary">{{.Event.Summary}}</span>
{{if .Event.Location}}<span class="loc muted">· {{.Event.Location}}</span>{{end}}
{{if .Event.Recurring}}<span class="recurring" title="recurring — only literal DTSTART shown">↻</span>{{end}}
</li>
{{end}}
</ul>
</div>
{{end}}
{{else}}
<p class="empty muted">No events in the next 7 days.</p>
{{end}}
</article>
{{else}}
<p class="card-collapsed muted">No upcoming events.</p>
{{end}}
{{if or .P.Issues (not $collapse)}}
<article class="card card-issues">
<header>
<h2>Open issues <small class="muted">({{.P.IssueTotal}})</small></h2>
</header>
{{if .P.Issues}}
<ul class="issue-list">
{{range .P.Issues}}
<li class="issue-row">
<a class="proj" href="/i/{{.Item.PrimaryPath}}">{{.Item.PrimaryPath}}</a>
<a class="iss" href="{{.Issue.HTMLURL}}" target="_blank" rel="noopener">#{{.Issue.Number}} {{.Issue.Title}}</a>
{{range .Issue.Labels}}<span class="label">{{.}}</span>{{end}}
{{if .Issue.Assignees}}<small class="muted">@ {{range $i, $a := .Issue.Assignees}}{{if $i}}, {{end}}{{$a}}{{end}}</small>{{end}}
<span class="upd muted" title="{{.Issue.UpdatedAt.Format "2006-01-02 15:04"}}">{{.UpdRel}}</span>
</li>
{{end}}
</ul>
{{else}}
<p class="empty muted">No open issues across linked repos.</p>
{{end}}
</article>
{{else}}
<p class="card-collapsed muted">No open issues.</p>
{{end}}
{{if or .P.RecentDocs (not $collapse)}}
<article class="card card-docs">
<header>
<h2>Recent documents <small class="muted">({{.P.RecentDocsTotal}}, last 30d)</small></h2>
</header>
{{if .P.RecentDocs}}
<ul class="doc-list">
{{range .P.RecentDocs}}
<li class="doc-row">
<span class="per">{{.PER}}</span>
<span class="ref-type ref-type-{{.Link.RefType}}">{{.Link.RefType}}</span>
{{if .Link.Note}}<span class="note">{{deref .Link.Note}}</span>{{end}}
<a class="ref-id" href="{{.Link.RefID}}" target="_blank" rel="noopener">{{.Link.RefID}}</a>
<a class="proj muted" href="/i/{{.ItemPath}}">{{.ItemPath}}</a>
</li>
{{end}}
</ul>
{{else}}
<p class="empty muted">Nothing dated in the last 30 days.</p>
{{end}}
</article>
{{else}}
<p class="card-collapsed muted">No recent documents.</p>
{{end}}
{{if .P.Stale}}
<article class="card card-stale">
<header>
<h2>Stale projects <small class="muted">({{.P.StaleTotal}}) · consider archiving?</small></h2>
</header>
<ul class="stale-list">
{{range .P.Stale}}
<li class="stale-row">
<a class="proj" href="/i/{{.Item.PrimaryPath}}">{{.Item.PrimaryPath}}</a>
<span class="repo muted">{{.Repo}}</span>
<span class="last-active" title="{{.LastActive.Format "2006-01-02"}}">last active {{.StaleRel}} ago</span>
</li>
{{end}}
</ul>
</article>
{{end}}
</section>
</section>
{{end}}