<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Caldav on Manuel Herrmann</title>
    <link>https://blog.0x17.de/tags/caldav/</link>
    <description>Recent content in Caldav on Manuel Herrmann</description>
    <image>
      <title>Manuel Herrmann</title>
      <url>https://blog.0x17.de/images/mh.jpg</url>
      <link>https://blog.0x17.de/images/mh.jpg</link>
    </image>
    <generator>Hugo -- 0.164.0</generator>
    <language>en</language>
    <lastBuildDate>Fri, 04 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog.0x17.de/tags/caldav/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>DavPunk: A Kanban Board on a Protocol That Has No Columns</title>
      <link>https://blog.0x17.de/post/davpunk-kanban-over-caldav/</link>
      <pubDate>Fri, 04 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.0x17.de/post/davpunk-kanban-over-caldav/</guid>
      <description>Capture a task on my phone, work it on a board on my desk, with no service in the middle. CalDAV has carried the tree all along - the only thing missing is one property for the column.</description>
      <content:encoded><![CDATA[<p>I collect hobbies. Soldering, 3D printing, things the house needs, servers that
want rebuilding, software I started and put down. Every one of them leaves a
residue of things I meant to do, and they all end up on the same list, where most
of them sleep for months - the mood for soldering does not arrive on a schedule.</p>
<p>Then an evening opens up and the mood is there, and the only question worth
answering is: what was I in the middle of? Not what is overdue. Nothing here is
overdue, because none of it was ever due. What I need to see is which of these
hobbies has something already in flight, and which one has a heap of things
waiting to be picked up.</p>
<p>A list cannot tell me that. A list sorts by date, and a backlog of hobbies has no
dates in it, so the two things I am actually working on sit somewhere in the
middle of everything I am not, in whatever order a computer found least
objectionable.</p>
<p>That is not a rendering failure. It is the format being honest. A CalDAV task
collection is a bag of VTODO components, and a VTODO knows how to say what it is
called, when it is due, whether it is done and which task it hangs under. It has
no opinion whatsoever about columns.</p>
<p>I wanted columns. So DavPunk is a CalDAV client that draws a kanban board over
task collections it does not own, on a protocol with no board in it, without
writing anything the next client cannot read.</p>
<p><img alt="intro" loading="lazy" src="/post/davpunk-kanban-over-caldav/intro.jpg"></p>
<hr>
<h2 id="why-not-just-use-a-kanban-board">Why Not Just Use a Kanban Board</h2>
<p>The obvious objection deserves an answer before anything else: Trello, Vikunja,
Nextcloud Deck and Kanboard are all better boards than mine. Swimlanes,
assignees, attachments, comments, sharing, a history per card. If a board is what
you want, that is where to get one.</p>
<p>What I need underneath the columns is depth: a story, the tasks that make it up,
and subtasks under those - and now and again one more level below that. That is
the half of this post that is not about columns at all.</p>
<p>Nextcloud is the one I keep coming back to, and I have looked at it more than
once. I ran one years ago. It does a great many things at the same time and does
them for a great many people, and that is precisely the shape that does not fit
me: I would rather run a few tools that each have one job than one tool that has
all of them - not least because the all-in-one is the thing you are still
upgrading on a Sunday evening. Letting mine go took a standing piece of
maintenance off the list. What I kept was the part I actually needed, and that is
how I ended up with Radicale holding my calendars and my todos and nothing else.</p>
<p>That preference is this whole post one layer down: small things that do their
job, and a format everything agrees on underneath them.</p>
<p>What I was after, then, sits one step to the side of what a board tool is for. On
the sofa, on a phone, I want to write down five things before I forget the third,
and in a board-first tool that naturally goes through the board&rsquo;s own app,
because there the board <em>is</em> the database.</p>
<p>So try it the other way round. Make the storage the standard and the board a
<em>view</em>, and the device you capture on no longer needs to know that a board
exists. My phone writes a VTODO through DAVx5 into that same Radicale collection:
a summary and nothing else, because a thing you thought of in the kitchen has no
status, no due date and no column yet.</p>
<p>And that is exactly why it lands in the right place. &ldquo;To Do&rdquo; is the column for
tasks carrying no <code>STATUS</code> at all, so a task written by an app that has never
heard of DavPunk arrives in the pool on Monday morning. I did not build an inbox.
A task with no status <em>is</em> the inbox, and the phone gets it right by not trying.</p>
<p>The desk then does the half a phone is bad at: seeing all of it at once,
dragging a family around, working out what is actually in flight this week. Two
devices, two shapes of one collection, on a server that does not know either of
them is special.</p>
<p><img alt="capture" loading="lazy" src="/post/davpunk-kanban-over-caldav/capture.jpg"></p>
<p>The honest cost is everything a board-first tool has that a VTODO has no room
for. No assignees, no comments, no attachments, no activity feed. I could mint
<code>X-</code> properties for all of them and nothing else in the world would read them.
And the column I set on the desk is invisible on the phone, which is a real
problem and the one I want to end on.</p>
<p>So if you have thirty tasks and a phone, none of this is worth it. It is for the
person whose backlog outgrew a list years ago and who would like to see it as
work in flight rather than as an accusation.</p>
<hr>
<h2 id="what-the-format-gives-you-and-what-it-refuses-to">What the Format Gives You, and What It Refuses To</h2>
<p>Open one of these tasks and you find what you would expect: a title, a due date,
whether it is finished, how far along it is, some tags. A status, with four
values and the option of setting none. And a link to the task it belongs
underneath, which is a real relationship rather than a convention, and the reason
subtasks work at all.</p>
<p>It does not hand you a column. It does not hand you a position within a list
either: two tasks with the same due date have no defined order, and no property
in the standard says which one you dragged above the other.</p>
<p>The appetite for joining these two worlds is real and it has been for years, and
so far it has been approached from the side that is easier to stand on. Nextcloud
Deck owns its own database of boards, stacks and cards, and the work there maps
that database <em>out</em> to the calendar format: boards to VCALENDAR, stacks to lists,
cards to VTODO, in <a href="https://github.com/nextcloud/deck/issues/15">an issue opened in
2017</a> with a bounty attached and
<a href="https://github.com/nextcloud/deck/issues/2399">write access</a> taken up after it.
When you own the schema a column is simply a column, and you can offer a view of
it to everyone else.</p>
<p>Coming from the other side is the direction still open. The column has to be an
<em>interpretation</em> of data somebody else&rsquo;s client wrote, it has to survive
round-tripping through servers and clients that have never heard of it, and it
has to still be a column tomorrow after a phone app has edited the same task.
That is the harder half, and it is also the half where the payoff is: get it
right once and the board is no longer tied to the application that drew it.</p>
<p>DavPunk is not a Trello clone with a CalDAV export, and it is not a server. My
Radicale did not change. It still holds plain VTODO that any other client reads
normally. The board exists in the reading.</p>
<hr>
<h2 id="the-column-is-something-i-made-up">The Column Is Something I Made Up</h2>
<p>Which column a card sits in comes down to three questions, asked in order. Does the
task carry a column of its own, put there by a drag? Then that one. Otherwise, is
there a column standing for the status it has? Then that one. And otherwise - only
otherwise - it is not on the board.</p>
<p>With the board as it ships, that third case never happens: there is a column for
every status a task can have, including the one that has none. It exists so that a
column can be <em>optional</em>, which is the point I will come back to in a moment.</p>
<p>The first question is the interesting one, because it is how the board says
something <code>STATUS</code> cannot. Two columns can stand for the same status - &ldquo;waiting on
a part&rdquo; and &ldquo;waiting on me&rdquo; are both, as far as iCalendar is concerned, the
identical kind of not-started - and dragging a card writes the column beside the
status so the distinction survives. If I later delete that column from the config,
the card quietly falls back to its status and is filed again the next time I drag
it, and the property is left alone in the meantime. Carrying a property you do
not understand costs nothing, and it is what lets two programs share a task
without either having to know about the other.</p>
<p>The interesting one is the column with no status at all:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-toml" data-lang="toml"><span style="display:flex;"><span>[<span style="color:#a6e22e">davpunk</span>.<span style="color:#a6e22e">kanban</span>]
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">columns</span> = [
</span></span><span style="display:flex;"><span>  {<span style="color:#a6e22e">id</span> = <span style="color:#e6db74">&#34;todo&#34;</span>,        <span style="color:#a6e22e">label</span> = <span style="color:#e6db74">&#34;To Do&#34;</span>                                },
</span></span><span style="display:flex;"><span>  {<span style="color:#a6e22e">id</span> = <span style="color:#e6db74">&#34;needsaction&#34;</span>, <span style="color:#a6e22e">label</span> = <span style="color:#e6db74">&#34;Needs Action&#34;</span>, <span style="color:#a6e22e">status</span> = <span style="color:#e6db74">&#34;NEEDS-ACTION&#34;</span>},
</span></span><span style="display:flex;"><span>  {<span style="color:#a6e22e">id</span> = <span style="color:#e6db74">&#34;inprogress&#34;</span>,  <span style="color:#a6e22e">label</span> = <span style="color:#e6db74">&#34;In Progress&#34;</span>,  <span style="color:#a6e22e">status</span> = <span style="color:#e6db74">&#34;IN-PROCESS&#34;</span>  },
</span></span><span style="display:flex;"><span>]
</span></span></code></pre></div><p><code>todo</code> declares no status, so it collects the tasks that carry none. <strong>No
<code>STATUS</code> is a value, not a gap.</strong> A task nobody has looked at and a task somebody
explicitly marked <code>NEEDS-ACTION</code> are different states of the world - a pool you
pick from, and work that has been picked - and collapsing them into one column
loses exactly the distinction a board is for. They shared a column here at first,
and splitting them is the single change that made the board readable. You can see
the shape of it in the screenshot: an enormous pool, a shorter column of things
somebody has picked up, a very short one of what is genuinely running. That last
column is the only one I look at on a Tuesday evening.</p>
<p>Dropping a card back on <code>todo</code> <em>clears</em> <code>STATUS</code>. Going back to the pool has to
be able to undo having picked something up, otherwise the pool only ever drains.</p>
<p>Rule 3 is what makes a column optional. Delete Done and Cancelled from the config
and finished cards leave the board with them - they do not pile into the first
column, which would make the board worse rather than smaller. Nothing is lost:
the list view and the search still show everything, the states stay reachable
from <em>Edit - Change status</em>, and the board prints the count it is not showing.
&ldquo;Nothing to do&rdquo; and &ldquo;I am not showing you this&rdquo; must never look the same.</p>
<p>Deleting a column is the heavy version of not wanting to look at something,
though, and it takes the drop target with it - there is then nowhere to <em>put</em> a
finished card. So clicking a column&rsquo;s name folds it instead: it collapses to a
spine one line wide at the right edge of the board, name and count turned on
their side, and goes on accepting drops along its whole height. Folded, <code>Done</code>
is an easier target than it was open.</p>
<p><img alt="the board" loading="lazy" src="/post/davpunk-kanban-over-caldav/davpunk.png"></p>
<p>That is my actual list, titles blurred. Two folded spines on the right, the
filter bar across the top, and every card a tree.</p>
<hr>
<h2 id="a-story-its-tasks-and-their-subtasks">A Story, Its Tasks, and Their Subtasks</h2>
<p>The columns are the half that shows in a screenshot. The other half is the one I
would miss more.</p>
<p>What I have is not a list of tasks. It is a handful of stories with work hanging
underneath them. &ldquo;Enclosure for the printer&rdquo; is not a task, it is a thing that
has tasks under it, and some of those have tasks of their own: measure the frame,
order the acrylic, and under that the two dimensions I have to check before I can
order anything at all. A card with a checklist inside it does not describe that.
A tree does - and iCalendar has carried the parent link for this the whole time,
so the depth is not something I invented either. It is in the data on the server,
and any client that reads the link can see it.</p>
<p>So both views are trees, as deep as I need them. That is the piece that kept me
from simply moving to a board tool, and it is where the two ideas start pulling
against each other. A column is a <em>status</em>. Nesting is a <em>relation</em>. They are
independent: the enclosure can be in progress while three of the things under it
are still in the pool and one is finished. Every column is a slice of the whole
list, and a slice cuts families in half.</p>
<p>Draw that naively and you get cards that make no sense. A subtask sitting at the
root of &ldquo;To Do&rdquo; reads as a task in its own right, and the one thing that would
explain it - what it hangs under - is exactly what the slice threw away.</p>
<p><img alt="columns and nesting" loading="lazy" src="/post/davpunk-kanban-over-caldav/columns-vs-nesting.png"></p>
<p>So the board shows the family either way. A subtask whose parent is elsewhere
brings that parent along as a grey row above it. A parent whose subtask has gone
to another column keeps a grey stand-in for the child instead of sitting there
looking like there is nothing left to do. The chain walks up until it reaches
something the column already shows, so a parent here and a grandparent elsewhere
still nest the way they look.</p>
<p>The grey is not decoration. It is the answer to the question a board is supposed
to answer at a glance: what is in flight, and what is next. A card sitting in
Needs Action means one thing on its own and something quite different once you
can see the parent above it is already in progress - that is not a loose task,
that is the next step of something already running. And a parent in progress with
two grey children waiting in other columns is telling you exactly what it is
still short of.</p>
<p>A grey row stands for a task that lives somewhere else, so it cannot be dragged,
ticked or dropped onto: each of those would be a claim about a column the task is
only visiting. Right-click it and the menu works normally, because the menu names
the task rather than the row, and acts on the real one.</p>
<p>In Progress is deliberately the exception, and the reason is not screen space. In
Progress is what I am doing <em>now</em>, and the moment it also shows everything
hanging underneath what I am doing, it stops being a list of work and turns into
a project plan. That column has one job: keep it short, finish what is in it, and
if something is not going to be finished, put it back in Needs Action where it
can wait without pretending. So it does not pull the family down into itself
unless I ask it to. Parents still come along, because a subtask with nothing
above it is unreadable in any column - it is only the walk downward that stays
off, in the one place worth protecting from itself.</p>
<p>And once everything is a tree, you get something nobody had to design: a folder.</p>
<p><img alt="backlog" loading="lazy" src="/post/davpunk-kanban-over-caldav/backlog.jpg"></p>
<p>There is a task in my To Do column called <code>backlog</code>. It has no due date and it is
never going to be finished. Everything I might eventually pick up goes underneath
it, and folded shut it is a single line on the board with a count beside it. All
the things I am not doing this month are still there, still searchable, still
mine - and they are not looking at me.</p>
<p>Then the mood arrives, and I take one out. Pull it up to the top level and it
stops being something I might do and becomes something I am doing. That one
gesture is the entire workflow. None of it is a feature: a folder is just a task
with other tasks under it, and iCalendar could always say that much. But it turns
a flat pile of everything into places where things belong, and it is what lets
the board follow my mood instead of a schedule.</p>
<hr>
<h2 id="gestures-that-mean-what-the-data-means">Gestures That Mean What the Data Means</h2>
<p>Once columns and nesting are independent, every drag has to say which of the two
it is changing. The board answers by where you let go.</p>
<ul>
<li><strong>Onto a card</strong> - it becomes that card&rsquo;s subtask and takes its column with it.</li>
<li><strong>Between two cards</strong> - it becomes their sibling. This is the only drag on the
board that gets a subtask back <em>out</em> of its parent: let go between two
top-level cards and it is top-level too.</li>
<li><strong>Onto a column, or onto its name</strong> - the status changes and nothing else. A
column says nothing about what a task hangs under, so it leaves the family
alone.</li>
</ul>
<p>The column&rsquo;s name being a target matters more than it sounds. Aiming at the word
&ldquo;Done&rdquo; is far easier than aiming at the empty space under the last card, which in
a busy column is not on screen at all. But an invisible target is no target, so
picking a card up lights every heading with a dashed outline and fills in the one
under the cursor. The board tells you where you are allowed to let go, instead of
letting you find out afterwards whether you guessed right.</p>
<p>Then there is the thing a board cannot do at all. Dragging needs both cards on
screen, and a filter is precisely what stops that: I want this soldering task
underneath that project, and the project lives in a list I filtered away twenty
minutes ago. So a task can be picked up, the filter changed underneath it, and
the task put down somewhere that was not visible when I grabbed it. It is a move
and not a copy, and it never touches the system clipboard - the whole point is to
let the two halves of one thought happen at different times.</p>
<p>The one I reach for most is a plain text box. Five things arrive at once - the
parts for a project, the four bits I need to order before Saturday - and the full
editor is the wrong shape for that: a dozen fields per item, and every one of
them is something you have no answer for yet. So there is a box where one line is
one subtask. Paste from anywhere, and bullets,
checkbox brackets and numbering are stripped on the way in, because a task called
<code>- [ ] Buy milk</code> is a checkbox nothing will ever tick. Indentation is ignored
rather than read as structure: a tree inferred from a paste is a tree nobody
asked for.</p>
<p>A folded subtree stays folded, through every refresh and every restart, and says
how many things it is hiding while it does. That sounds like nothing until it is
the backlog: a parent with a couple of hundred things under it, where unfolding
by default would bury the rest of the board and hand you back exactly the pile
you were trying not to look at. The rule underneath is that &ldquo;never seen before&rdquo; has to stay
distinguishable from &ldquo;I closed this on purpose&rdquo;, so a group is open unless it was
closed, and a subtree is closed unless it was opened.</p>
<p>The filters remember, too, and they remember selectively. Which lists and tags I
picked survive a restart, because I work out of the same one or two lists for
weeks at a time and choosing them again every morning is a chore. What I typed
into the search box does not, because a search is typed for one question and then
answered. That memory lives in DavPunk&rsquo;s own state file rather than in the config
I wrote: nothing the app keeps for itself belongs in a file that is mine.</p>
<hr>
<h2 id="when-two-clients-disagree-nobody-wins-automatically">When Two Clients Disagree, Nobody Wins Automatically</h2>
<p>Every edit lands locally first and is pushed afterwards, so the network is never
in the way of typing. The price is that the phone and the desk can both change
the same task before either has told the server, and the cheap way out is to let
whichever arrived last stand.</p>
<p>DavPunk asks instead. The task is flagged, refuses further edits, and
opens a window with three columns: the server&rsquo;s version, the result, and mine.
One row per field, the ones that actually differ marked and the rest dimmed. Push
either side&rsquo;s value into the middle, or type something into the middle that
neither side had. The middle is what gets saved. You can also decline to decide,
and it comes back at a later sync, which is the right answer more often than I
expected.</p>
<p>The middle starts on whichever version is newer <em>as a whole</em>, because iCalendar
keeps no timestamp per field and there is no honest way to decide &ldquo;latest&rdquo; one
field at a time. Mine wins a tie, on the grounds that it is the edit I can still
remember making.</p>
<p>Some things refuse to be separated, and the window moves them together. A due
date carries its time zone with it, because the server&rsquo;s time wearing my zone is
a different instant. A status carries the column it was set beside, because a status
from one side with a column from the other puts the card somewhere its own status
contradicts.</p>
<p>And one thing never reaches that window at all. Dragging a card writes down where
it now sits, so two clients tidying the same list collide constantly - and
opening three panes to ask &ldquo;which order did you mean&rdquo; would be an insult. Those
are settled on the spot, newest wins, and you see them counted rather than
queued. A position is dragged and never typed, so getting it wrong costs a
re-drag rather than a sentence somebody wrote. A deletion never qualifies: a
deletion is a decision, not a merge.</p>
<hr>
<h2 id="a-sync-you-can-read-before-you-run-it">A Sync You Can Read Before You Run It</h2>
<p>The first time you aim a new client at a server that already holds years of your
tasks, you press sync and you hope. That is a terrible feeling to design for, so
there is a preview: what it would send, and what it would change here, without
doing either.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>davpunk sync --dry-run
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>ox: 8 calendar(s)
</span></span><span style="display:flex;"><span>  to the server: 2 request(s)
</span></span><span style="display:flex;"><span>    CREATE Buy milk           (/dav/ox/einkaufen/a1b2.ics, 412 bytes)
</span></span><span style="display:flex;"><span>    UPDATE Fix the gate       (/dav/ox/haus/c3d4.ics, 508 bytes)
</span></span><span style="display:flex;"><span>  to the local cache: 12 new, 3 updated, 0 removed, 1 conflict(s)
</span></span><span style="display:flex;"><span>    new      Bagger ausleihen
</span></span><span style="display:flex;"><span>    upd      Regentonne  [summary, due_value]
</span></span><span style="display:flex;"><span>    CONFLICT Kraut und Rüben
</span></span></code></pre></div><p>The reading half is real. It talks to the server, compares what is there against
what it has, and tells you what it found - so a wrong password, a list that has
gone missing or a task it cannot parse turns up in the preview exactly as it
would in the real thing. What it cannot predict is how the server would answer a
write it never sent.</p>
<p>It also cannot cheat, which is the part I care about. The paths that send changes
are physically disconnected for a preview, and the whole run happens against a
throwaway copy of the local database. Otherwise it is the same code as a real
sync rather than a second implementation quietly drifting away from the first.
The same preview is in the window, with a <em>Sync now</em> button underneath it for
when you like what you see.</p>
<p>And if a list is skipped because the server says nothing in it has changed, the
report says so. &ldquo;Nothing to do&rdquo; and &ldquo;I did not look&rdquo; must never read the same.</p>
<hr>
<h2 id="where-the-password-lives">Where the Password Lives</h2>
<p>DavPunk supports a desktop keyring and it supports GnuPG, and I use GnuPG. Not
out of purity: my key lives on a YubiKey that is already in the port, it comes
with me between machines, and so my credentials come with me. The encrypted
password can sit in a backup, in a synced folder, on a disk somebody walks off
with, and it is worth precisely nothing without the token.</p>
<p>That also settles what the background sync does when it cannot read the password,
which is nothing at all. It does not throw a PIN prompt at me from behind
whatever I am typing, and it does not ask a keyring to open on my behalf. It
skips the round, mentions it once an hour, and the window says the credentials
are locked. Pull the key and syncing stops. Put it back and it picks up where it
was. I like that it is boring.</p>
<hr>
<h2 id="a-shopping-list-scaled-to-six">A Shopping List, Scaled to Six</h2>
<p>Here is the errand I actually want handed off. Take a recipe written for four,
scale it to six, and put every ingredient on the shopping list. Two minutes of
typing and a small amount of arithmetic, and no part of it is something I want to
be doing myself.</p>
<p>So DavPunk can hand its tasks to an assistant, and the part worth saying is not
the protocol. It is that the assistant works where I work: through the same door
the window does, into the same local cache, so a dozen new subtasks under &ldquo;Sunday
dinner&rdquo; queue, sync and merge exactly the way mine do, offline included.
Everything is off until it is granted, and being allowed to add things is a
separate grant from being allowed to remove them: something that fills a shopping
list has no business emptying one.</p>
<hr>
<h2 id="the-missing-half-is-a-property-not-a-client">The Missing Half Is a Property, Not a Client</h2>
<p>A board over CalDAV comes down to two facts a VTODO cannot carry: which column a
task is in, and where it sits among its siblings. That is the entire
specification. Everything else in this post is one client&rsquo;s opinion about how to
draw it. Here is a card on the board above, as it sits on the server:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>BEGIN:VTODO
</span></span><span style="display:flex;"><span>UID:6f1c9d24-...
</span></span><span style="display:flex;"><span>SUMMARY:Order tiles
</span></span><span style="display:flex;"><span>STATUS:IN-PROCESS
</span></span><span style="display:flex;"><span>RELATED-TO;RELTYPE=PARENT:3ab97e10-...
</span></span><span style="display:flex;"><span>X-DAVPUNK-KANBAN-COL:inprogress
</span></span><span style="display:flex;"><span>X-DAVPUNK-ORDER:3000
</span></span><span style="display:flex;"><span>END:VTODO
</span></span></code></pre></div><p>Five of those lines are standard iCalendar that every task client already reads,
including the parent link that makes the tree. Two are mine, and both are in the
wrong namespace.</p>
<p>The second half is already solved, and by nobody in particular. Manual ordering
in Apple Reminders, Nextcloud Tasks, Tasks.org and elementary Tasks all runs on
<code>X-APPLE-SORT-ORDER</code>, a vendor-prefixed extension from a vendor most of those
projects have nothing to do with. <a href="https://tasks.org/docs/manual_sort_mode/">Tasks.org documents
it</a> as &ldquo;a non-standard extension to the
iCalendar protocol&rdquo;, compatible with Apple Reminders and Nextcloud Tasks, and
<a href="https://github.com/elementary/tasks/pull/198">elementary&rsquo;s implementation</a> even
matches Apple&rsquo;s rule for a missing value: the seconds between the task&rsquo;s creation
and the Cocoa epoch, <code>20010101T000000Z</code>. Four clients, no RFC, no working group,
no announcement. It converged because a private ordering is worth nothing and a
shared one is worth something.</p>
<p>Which makes my <code>X-DAVPUNK-ORDER</code> the wrong call by my own argument. It is a
position nobody else can read, in the one slot where the ecosystem has already
agreed on an answer. I would rather be talked out of it than defend it, and the
fix is to read and write what everybody else does.</p>
<p>That leaves exactly one property genuinely missing: the column. A free-form
string, meaningful to whoever set it, plus three rules for reading it - the
property wins if it names a column you have, otherwise <code>STATUS</code> picks the column,
otherwise the task is not on the board. That is the whole proposal. It is shorter
than the paragraph above it, and <code>X-APPLE-SORT-ORDER</code> is proof that this is how
such things actually get adopted.</p>
<p>And what it asks of everybody else is smaller still: <strong>keep the properties you do
not recognise.</strong> That is all. A client that hands back what it was given, plus
its own changes, is already doing the whole job - and every feature that any two
clients could ever share downstream depends on that one habit. DavPunk holds to
it strictly, writing back every calendar-level property, every time zone
definition and every neighbouring component exactly as it found them, and
touching only the handful of fields it means to change. It costs nothing, and it
is what makes somebody else&rsquo;s column safe in my hands.</p>
<p>Then the board stops being a feature of one application and becomes a property of
the data. The phone in the kitchen shows the column a card was put in, without
either app having heard of the other. A list shared with somebody - which CalDAV
has done for years - becomes a shared board without anyone having built one. And
the day I move to a different client, the board comes along, because it was never
the client&rsquo;s to keep. That is the version worth having, and the missing piece is
one property.</p>
<hr>
<h2 id="where-this-goes">Where This Goes</h2>
<p>About half of what is in that repository is not code at all. It is a design
document, and it is the authority rather than a description: every rule in this
post was a paragraph in there before it was a behaviour, and the tests assert the
document rather than the implementation.</p>
<p>That sounds like ceremony right up to the day the meaning of the board changes
underneath you, which it did three times. Splitting the pool of untouched things
out of the work somebody had picked up. Making a column something you can simply
delete. Teaching a column to show the family it only half contains. Not one of
those reached the syncing half of the program, because the document already knew
where the seam was. Knowing where a rule <em>lives</em> is what let me keep changing my
mind about the board without being frightened of it.</p>
<p>The code is not public yet. The threads I would pull next, in order:
<code>X-APPLE-SORT-ORDER</code> in place of my own ordering property, so a position I drag
here means something over there; repeating tasks properly, so a series with
per-occurrence exceptions stops being read-only just because rewriting it could
damage what another client made; reminders that arrive with the app closed; and a
second server to
test against, because right now <a href="https://radicale.org/">Radicale</a> is the only one
the tests spin up, and &ldquo;works with CalDAV&rdquo; only means something once it is
several servers.</p>
<p>The tree was in the format the whole time. The parent link has been sitting in
iCalendar for years and DavPunk only draws it, which is why a story with tasks
and subtasks under it needed no invention at all. What is genuinely missing is
smaller than that: which column a card is in, and where it sits among its
siblings. Those two are the difference between a backlog of sleeping hobbies and
a board that tells you which of them are awake. The format not having them yet is
no reason for a client not to - and the day a second client writes the same
column property, it stops being my board and starts being my data.</p>
<p>Which leaves one thing that is nobody&rsquo;s business but mine. I had been using it
for weeks before I worked out what was still missing: it did not look like my
desk. So today I gave DavPunk a theme, the quiet colours I already live in.
Nothing about the syncing changed, nothing about the board changed. But something
you open every morning is a place rather than a utility, and this one finally
feels like home.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
