m's CalDAV server (dav.msbls.de, SabreDAV) now feeds projax via a thin
read-only-plus-create-on-demand integration. No background sync; tasks
fetched live on detail-page render.
New caldav/ package
- ListCalendars (PROPFIND Depth: 1, filters non-calendar collections)
- ListTodos (REPORT calendar-query for VTODO; hand-rolled iCalendar
parser for UID/SUMMARY/STATUS/DUE/PRIORITY/LAST-MODIFIED — RFC 5545
line-folding aware)
- CreateCalendar (MKCALENDAR, 405 → ErrCalendarExists for the "link
instead" branch)
- httptest-stubbed tests cover all four paths.
Store
- ItemLink shape + LinksByType / LinksByRefType / AddLink / DeleteLink.
AddLink upserts on (item_id, ref_type, ref_id, rel) so re-linking the
same calendar is idempotent.
Web
- GET /admin/caldav — discovery + auto-suggested matches + manual
linker. Suggestion = lowercased displayname == projax slug or title.
- POST /admin/caldav/link — insert item_links row.
- POST /admin/caldav/unlink — delete by link id.
- POST /i/{path}/caldav/create — MKCALENDAR at <base>/<slug>/, then
AddLink. On 405 (already exists), fall back to link-only.
- Detail page Tasks section: per-calendar block with open VTODOs +
collapsed completed (30d window). Errors per calendar logged and
skipped, so one bad calendar does not blank the page.
- nav adds /admin/caldav link.
main.go
- DAV_URL + DAV_USER + DAV_PASSWORD optional. Missing DAV_URL → CalDAV
off (admin page renders "not configured" notice). DAV_URL set but
user/pass missing → fail fast at boot.
docs/design.md gains §5 documenting the integration shape.
deploy/dokploy.yaml lists the two new secrets + the env var.
Phase 2.b (writeback / two-way / background sync) is parked.
93 lines
3.4 KiB
Cheetah
93 lines
3.4 KiB
Cheetah
{{define "content"}}
|
|
<h1>{{.Item.Title}}</h1>
|
|
<p class="meta">
|
|
<span class="slug">{{.Item.PrimaryPath}}</span>
|
|
<span class="status status-{{.Item.Status}}">{{.Item.Status}}</span>
|
|
{{range .Item.Management}}<span class="mgmt mgmt-{{.}}">{{.}}</span>{{end}}
|
|
{{range .Item.Tags}}<span class="tag">{{.}}</span>{{end}}
|
|
{{if .Item.Pinned}}<span class="pin">pinned</span>{{end}}
|
|
{{if .Item.Archived}}<span class="archived">archived</span>{{end}}
|
|
{{if .Item.SourceRefDeref}}<span class="muted">mai id: {{.Item.SourceRefDeref}}</span>{{end}}
|
|
</p>
|
|
{{if .Item.OtherPaths}}
|
|
<p class="meta muted">Also at: {{range $i, $p := .Item.OtherPaths}}{{if $i}}, {{end}}<a href="/i/{{$p}}">{{$p}}</a>{{end}}</p>
|
|
{{end}}
|
|
|
|
{{if .CalDAVOn}}
|
|
<section class="tasks">
|
|
<h2>Tasks</h2>
|
|
{{if .Tasks}}
|
|
{{range .Tasks}}
|
|
<div class="cal-block">
|
|
<h3>{{.DisplayName}}</h3>
|
|
{{if .Open}}
|
|
<ul class="todo open">
|
|
{{range .Open}}
|
|
<li>
|
|
<span class="status status-{{.Status}}">{{.Status}}</span>
|
|
{{.Summary}}
|
|
{{if .Due}}<small class="muted">due {{.Due.Format "2006-01-02"}}</small>{{end}}
|
|
</li>
|
|
{{end}}
|
|
</ul>
|
|
{{else}}
|
|
<p class="muted">No open tasks.</p>
|
|
{{end}}
|
|
{{if .DoneRecent}}
|
|
<details>
|
|
<summary class="muted">{{len .DoneRecent}} completed in last 30 days</summary>
|
|
<ul class="todo done">
|
|
{{range .DoneRecent}}<li>{{.Summary}}</li>{{end}}
|
|
</ul>
|
|
</details>
|
|
{{end}}
|
|
</div>
|
|
{{end}}
|
|
{{else}}
|
|
<p class="muted">No CalDAV list linked.</p>
|
|
<form method="post" action="/i/{{.Item.PrimaryPath}}/caldav/create" class="inline">
|
|
<button type="submit">Create CalDAV list</button>
|
|
</form>
|
|
{{end}}
|
|
</section>
|
|
{{end}}
|
|
|
|
<form method="post" action="/i/{{.Item.PrimaryPath}}" class="edit">
|
|
<label>Title <input name="title" value="{{.Item.Title}}" required></label>
|
|
<label>Slug <input name="slug" value="{{.Item.Slug}}" required pattern="[^.]+"></label>
|
|
<label>Parents <small class="muted">(hold Ctrl/Cmd to pick multiple — same row can live under several branches)</small>
|
|
<select name="parent_ids" multiple size="6">
|
|
{{range .ParentOptions}}
|
|
<option value="{{.ID}}" {{if contains $.Item.ParentIDs .ID}}selected{{end}}>{{.Path}}</option>
|
|
{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Status
|
|
<select name="status">
|
|
{{range $opt := .StatusOptions}}
|
|
<option value="{{$opt}}" {{if eq $opt $.Item.Status}}selected{{end}}>{{$opt}}</option>
|
|
{{end}}
|
|
</select>
|
|
</label>
|
|
<label>Tags
|
|
<input name="tags" value="{{join "," .Item.Tags}}" placeholder="comma-separated, e.g. work, dev">
|
|
</label>
|
|
<label>Management
|
|
<input name="management" value="{{join "," .Item.Management}}" placeholder="comma-separated: self, mai, external">
|
|
</label>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="pinned" value="1" {{if .Item.Pinned}}checked{{end}}> pinned
|
|
</label>
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="archived" value="1" {{if .Item.Archived}}checked{{end}}> archived
|
|
</label>
|
|
<label>Content
|
|
<textarea name="content_md" rows="14">{{.Item.ContentMD}}</textarea>
|
|
</label>
|
|
<div class="actions">
|
|
<button type="submit">Save</button>
|
|
<a class="cancel" href="/">Cancel</a>
|
|
</div>
|
|
</form>
|
|
{{end}}
|