Commit Graph

10 Commits

Author SHA1 Message Date
mAi
d518978edb refactor(timeline): cache via internal/cache.TTLCache
Phase 5b slice C. Mirror of slice B for the timeline cache:
timelineCache + cachedTimeline + newTimelineCache deleted. The Server's
timeline field is now `*cache.TTLCache[*TimelinePayload]` constructed
via `cache.NewTTL[*TimelinePayload](timelineCacheTTL)`. Call sites
across web/{timeline,caldav,dashboard,links}.go renamed:

- s.timeline.get(k)        → s.timeline.Get(k)
- s.timeline.set(k, p)     → s.timeline.Set(k, p)
- s.timeline.invalidateAll → s.timeline.InvalidateAll
- (timeline never used keyed invalidate, so no .Invalidate rename)

Removes the unused `sync` import from web/timeline.go. The 50-line
timelineCache struct + four methods are gone; the file shrinks by
~50 lines.

All web/timeline_*test.go pass unmodified.

Task: t-projax-5b-cache
2026-05-22 00:27:08 +02:00
mAi
085e672dd5 refactor(dashboard): cache via internal/cache.TTLCache
Phase 5b slice B. dashboardCache deleted. The Server's dashboard field
is now `*cache.TTLCache[*dashboardPayload]` constructed via
`cache.NewTTL[*dashboardPayload](dashboardCacheTTL)`. All call sites
renamed:

- s.dashboard.get(k)         → s.dashboard.Get(k)
- s.dashboard.set(k, p)      → s.dashboard.Set(k, p)
- s.dashboard.invalidate(k)  → s.dashboard.Invalidate(k)
- s.dashboard.invalidateAll  → s.dashboard.InvalidateAll
  (across web/dashboard.go, web/server.go, web/caldav.go,
   web/links.go, web/gitea_writeback.go)

The 64-line dashboardCache struct + methods are gone; the dashboard
file shrinks by ~63 lines. TTL constant lifted out to
`dashboardCacheTTL = 60 * time.Second` so the const lives next to its
semantics rather than a magic-number literal in New().

All web/dashboard_*test.go pass unmodified.

Task: t-projax-5b-cache
2026-05-22 00:25:13 +02:00
mAi
ea0fb21069 refactor(dashboard): consume internal/aggregate/
Phase 5a slice C. collectTasks / collectIssues / collectEvents each
become 10-15 line shims that ask the aggregator for typed rows and
project them into the dashboard-flavoured display types.

- collectTasks: aggregator.Todos → filter status → bucket by due
  distance (overdue/today/tomorrow/week/no-due) → cap 30.
- collectIssues: aggregator.Issues → relativeTime label → sort by
  updated desc → cap 30. The Gitea TTL cache is now shared with the
  detail page through the aggregator.
- collectEvents: aggregator.Events with the [now, now+7d) window →
  EventStartLabel + dayLabelFor projection → group by day. Sort + cap
  semantics unchanged. CalendarRef field on dashboardEvent is no
  longer surfaced (kept for backwards compat).
- Dead eventStartLabel helper removed; aggregate.EventStartLabel is
  the canonical implementation now.

collectStale stays in dashboard.go — it has dashboard-specific
"is-this-item-quiet" reduce logic the aggregator doesn't model.

All dashboard tests (dashboard_test, dashboard_events_test,
dashboard_edit_test) pass unmodified.

Task: t-projax-5a-aggregator
2026-05-22 00:07:31 +02:00
mAi
4e919babed refactor(timeline): consume internal/aggregate/
Phase 5a slice B. Replace web/timeline.go's hand-rolled fan-out + day
grouping with calls into the aggregator package.

- web/timeline.go: collectTimelineTodos + collectTimelineEvents +
  in-line day grouping deleted. buildTimeline now calls
  aggregator.Todos/Events/Docs/Creations, decorates each typed row
  with the template-friendly TimelineRow shape (PER, StartLabel,
  DurationHint), then hands rows to aggregate.BuildTimelineDays for
  sorting + sticky-pill markers + far-future fade.
- web/timeline.go: TimelineRow / TimelineDay are now type aliases for
  the aggregate package's versions (Phase 5a slice A introduced them
  with the same flat-field layout the templates already address).
- web/server.go: new Server.Aggregator() factory builds a fresh
  *aggregate.Aggregator wired to the server's current CalDAV/Gitea
  deps (so main.go can install those after web.New without a re-init
  hook).
- web/{gitea,dashboard,gitea_writeback,gitea_test}.go: issueCache
  methods capitalised (Get/Set/Invalidate) so the aggregator's
  IssueCache interface accepts *web.issueCache directly. No behaviour
  change.

All web/timeline_*test.go pass unmodified — the refactor preserves
output shape and template field paths.

Task: t-projax-5a-aggregator
2026-05-22 00:05:14 +02:00
mAi
5dcacff520 feat(phase 4b): dark/light theme toggle + file-upload permanently out-of-scope
## Slice A — explicit dark/light toggle

projax now ships with two palettes and a 1y cookie to remember the choice.
Dark is the new default; ☀ button in the header nav flips to light and
writes projax_theme=light. Server reads the cookie via themeFromRequest(r)
and injects Theme + ThemeColor into every template via the centralised
render(w, r, …) path, so first paint never flashes the wrong theme. Inline
JS in layout.tmpl handles the toggle without a server roundtrip.

Every panel colour now lives in a CSS variable under
:root[data-theme=dark|light]; the only hardcoded hex values left are
inside those two :root blocks. A future palette tweak is one edit, not
30 selectors. Graph node colours, kind-badges, highlights and warn/ok/bad
all have parallel dark/light values picked for contrast.

Standalone SVG download bakes the light palette inline because the
downloaded asset has no parent :root providing vars — m's existing
snapshots stay print-friendly regardless of his current cookie.

Login page keeps its embedded dark CSS — it's the gateway, intentionally
always dark.

Tests: TestThemeDefaultIsDark, TestThemeCookieRoundTrips,
TestThemeCookieUnknownFallsBackToDark, TestThemeTogglePagesShareSameTheme,
TestThemeToggleScriptPresent, TestThemeColorMetaHelper. Full suite green.

## Slice B — file-upload permanently out of scope (m, 2026-05-17)

docs/design.md moves "File uploads / in-projax storage" from the §3c
parked list to a permanent "Out of scope (decided 2026-05-17)" clause
with the rationale: PER is the cross-reference index, not the file
system. docs/standards/per.md gains the same explicit clause so future
shifts working from the PER standard see the constraint where they
look. Memory note filed so future workers don't re-propose multipart
uploads, attachments tables, or documents buckets.

## docs/design.md §13 Theming

Documents the toggle approach, cookie semantics, palette table, the
standalone-SVG carve-out, the login-page exception, and the 4b
out-of-scope (prefers-color-scheme detection, per-page overrides,
transitions on swap).
2026-05-17 18:14:08 +02:00
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
mAi
d49ad219a4 feat(phase 3l vevents): VEVENT support on dashboard — closes mgmt-parity gap
caldav package:
- Event struct: UID, Summary, Start, End, AllDay, Location, Description,
  Recurring, URL — read-only, no writeback
- ListEvents(ctx, calendarURL, ListEventsOpts{TimeMin, TimeMax}) issues
  REPORT calendar-query with server-side <c:time-range> filter
- parseVEvents handles DATE vs DATE-TIME (via hasDateOnlyParam since
  splitLine strips ;VALUE=DATE), RRULE-present → Recurring=true with NO
  expansion (literal DTSTART only)
- 2 unit tests: full parse (DATE-TIME, all-day, recurring), hasDateOnlyParam

web dashboard:
- dashboardEvent / dashboardEventGroup types
- collectEvents fans out 4-worker pool across every caldav-list link,
  fixed 7-day window from now, sort start-asc, cap 50, group by day
- dayLabelFor: Today / Tomorrow / weekday-day-month
- Events card on /dashboard between Tasks and Issues, with empty-collapse
- 2 integration tests with stubbed CalDAV: surfaces upcoming + DATE/RRULE
  rendering; empty-collapse with no links

design.md §5 (CalDAV) + §Dashboard updated; mgmt-teardown plan's one
blocking gap is now closed.
2026-05-16 00:57:52 +02:00
mAi
5a56ad91e5 feat(phase 3h gitea writeback): close/reopen/comment/create from projax
- gitea pkg: CloseIssue, ReopenIssue, CreateIssue, AddComment + ErrForbidden
  classification on 401/403. Client.do sets Content-Type on non-empty bodies.
- web handler: POST /i/{path}/issues/{close|reopen|comment|create}
  - authorisation guard: repo form value must match a gitea-repo item_link
    on the target item (rejects form-crafted writes to unrelated repos)
  - HTMX re-renders issues_section partial after each action
  - busts gitea per-repo cache (open + closed-recent) and dashboard 60s TTL
- templates: ✓ close button + reopen + collapsible comment box on every
  issue row; "+ new issue" disclosure per repo
- design.md §6 retitled "Phase 2.d read; 3h writeback" with auth/perm
  semantics + parked list
- 5 unit tests in gitea/, 5 integration tests in web/ covering happy paths
  + 403 → inline banner fallback
2026-05-15 19:22:11 +02:00
mAi
0c3507c6d7 feat(phase 3g dashboard polish): stale-projects card + refresh button + empty-collapse
- gitea.GetRepo returns FullName + UpdatedAt for the stale-card probe
- dashboard collectStale: mai-managed items + linked-repo updated_at >60d
  + zero open tasks + zero open issues. Sorted longest-stale first, ≤20.
  Multi-repo items need ALL repos quiet to count as stale. Reuses the
  4-worker pool + the already-aggregated task/issue counts from the
  Tasks / Issues cards (no extra DAV/Gitea fetches).
- dashboardCache.invalidate(key) busts a single filter's cache entry;
  ?refresh=1 routes through it so ↻ button gets fresh data.
- "updated Nm ago · cached/fresh" label + ↻ refresh link in dashboard
  chrome.
- Empty-card collapse: with no filter + zero rows the card renders as
  a one-line muted note instead of full chrome. Filter-active cards
  keep chrome so m can tell "filter hid it" from "nothing there".
- design.md §"Dashboard / daily-driver view" extended with the 4 new
  surfaces; the 3e "stale (3f)" out-of-scope line dropped.
- 5 new tests: stale-surface, stale-skip-recent, refresh-busts-cache,
  empty-collapse, filter-keeps-chrome. 2 unit tests for gitea.GetRepo.
2026-05-15 19:13:43 +02:00
mAi
f3e5adf358 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)
2026-05-15 18:59:52 +02:00