summaryrefslogtreecommitdiff
path: root/web
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-18 00:14:45 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-18 00:14:45 +0000
commitf08f06bef47aac2c9effb4cec650d99c2deb2dd7 (patch)
treeae06bd2e140c678e67f2879f21b07a4114a41f73 /web
parentbe4d606e9a1f5b068abcc21bbac58d1e4705ea1f (diff)
Rework Tasks tab: Chains section, checklist modal, project visibility
The flat Tasks-tab atom list was silently dumping every chain step (locked and unlocked) and dormant bucket-pool items in as ordinary undated cards, with no chain/project context and no protection against completing a locked step out of order. - CompleteNativeTask now rejects completing a locked chain task (ErrChainTaskLocked), mapped to 400 in both the widget and web complete-atom handlers. - Chain tasks and dormant bucket items are excluded from the flat atom list; a new "Chains" section shows one card per active/paused chain with the current step and N/M progress. - New chain checklist modal (GET /chains/{id}) lists every position in order with pause/resume/abandon -- the web view originally deferred as Android-only. - Fixed a real bug this surfaced: resuming a paused chain only flipped the status flag, never unlocking the deferred successor, so a chain paused right after a completion stayed stuck forever. SetChainStatus now catches up the deferred advancement on resume, idempotently. - Atom cards gained a project-name chip for general visibility. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'web')
-rw-r--r--web/templates/index.html17
-rw-r--r--web/templates/partials/chain-detail.html52
-rw-r--r--web/templates/partials/tasks-tab.html40
3 files changed, 109 insertions, 0 deletions
diff --git a/web/templates/index.html b/web/templates/index.html
index 19f1c7e..7be2c52 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -262,11 +262,15 @@
if (e.key === 'Escape') {
closeActionModal();
closeTaskModal();
+ closeChainModal();
}
});
function closeTaskModal() {
document.getElementById('task-edit-modal').classList.add('hidden');
}
+ function closeChainModal() {
+ document.getElementById('chain-view-modal').classList.add('hidden');
+ }
</script>
<!-- Task Edit Modal -->
@@ -282,6 +286,19 @@
</div>
</div>
+ <!-- Chain Checklist Modal -->
+ <div id="chain-view-modal" class="hidden fixed inset-0 bg-modal-overlay flex items-center justify-center p-4 z-50">
+ <div class="bg-modal backdrop-blur-sm rounded-lg max-w-lg w-full max-h-[80vh] flex flex-col" style="box-shadow: 0 0 20px black;">
+ <div class="p-4 border-b border-white/10 flex justify-between items-center flex-shrink-0">
+ <h2 class="font-medium text-white">Chain</h2>
+ <button onclick="closeChainModal()" class="text-white/40 hover:text-white">✕</button>
+ </div>
+ <div id="chain-view-content" class="overflow-y-auto">
+ <p class="p-4 text-white/50 text-sm">Loading...</p>
+ </div>
+ </div>
+ </div>
+
<div class="fixed bottom-0 right-0 p-2 text-[10px] text-white/20 pointer-events-none">
{{.BuildVersion}}
</div>
diff --git a/web/templates/partials/chain-detail.html b/web/templates/partials/chain-detail.html
new file mode 100644
index 0000000..3372f03
--- /dev/null
+++ b/web/templates/partials/chain-detail.html
@@ -0,0 +1,52 @@
+{{define "chain-detail"}}
+<div class="p-4 space-y-4">
+ <div class="flex items-center justify-between gap-2">
+ <div>
+ <h3 class="text-white font-medium">{{.ProjectName}}</h3>
+ <p class="text-xs text-white/50 mt-0.5">
+ {{.Chain.Status}}
+ {{range $i, $t := .Tasks}}{{if $t.ChainUnlocked}} &middot; step {{add $i 1}} of {{len $.Tasks}}{{end}}{{end}}
+ </p>
+ </div>
+ <div class="flex gap-2 flex-shrink-0">
+ {{if eq .Chain.Status "active"}}
+ <button hx-post="/chains/{{.Chain.ID}}/pause"
+ hx-on::after-request="if(event.detail.successful) { htmx.trigger(document.body, 'refresh-tasks'); closeChainModal(); }"
+ class="text-xs px-2 py-1 rounded bg-white/10 hover:bg-white/20 text-white/70">Pause</button>
+ {{else if eq .Chain.Status "paused"}}
+ <button hx-post="/chains/{{.Chain.ID}}/resume"
+ hx-on::after-request="if(event.detail.successful) { htmx.trigger(document.body, 'refresh-tasks'); closeChainModal(); }"
+ class="text-xs px-2 py-1 rounded bg-white/10 hover:bg-white/20 text-white/70">Resume</button>
+ {{end}}
+ <button hx-post="/chains/{{.Chain.ID}}/abandon"
+ hx-confirm="Abandon this chain? This can't be undone."
+ hx-on::after-request="if(event.detail.successful) { htmx.trigger(document.body, 'refresh-tasks'); closeChainModal(); }"
+ class="text-xs px-2 py-1 rounded bg-red-500/10 hover:bg-red-500/20 text-red-300/80">Abandon</button>
+ </div>
+ </div>
+
+ <div class="space-y-1">
+ {{range $i, $t := .Tasks}}
+ <div class="flex items-start gap-2 py-2 px-2 rounded-lg {{if $t.Completed}}opacity-50{{else if not $t.ChainUnlocked}}opacity-40{{end}}">
+ {{if $t.Completed}}
+ <span class="mt-0.5 text-green-400 flex-shrink-0">✓</span>
+ {{else if $t.ChainUnlocked}}
+ <input type="checkbox"
+ hx-post="/complete-atom"
+ hx-vals='{"id": "{{$t.ID}}", "source": "doot"}'
+ hx-on::after-request="if(event.detail.successful) { htmx.trigger(document.body, 'refresh-tasks'); htmx.ajax('GET', '/chains/{{$.Chain.ID}}', {target:'#chain-view-content', swap:'innerHTML'}); }"
+ class="mt-0.5 h-4 w-4 rounded bg-black/40 border-white/30 text-white/80 focus:ring-white/30 cursor-pointer flex-shrink-0">
+ {{else}}
+ <span class="mt-0.5 text-white/30 flex-shrink-0">🔒</span>
+ {{end}}
+ <div class="flex-1 min-w-0">
+ <span class="text-sm {{if $t.Completed}}line-through text-white/50{{else if $t.ChainUnlocked}}text-white{{else}}text-white/40{{end}}">{{add $i 1}}. {{$t.Content}}</span>
+ {{if $t.Description}}
+ <p class="text-xs text-white/40 mt-0.5">{{$t.Description}}</p>
+ {{end}}
+ </div>
+ </div>
+ {{end}}
+ </div>
+</div>
+{{end}}
diff --git a/web/templates/partials/tasks-tab.html b/web/templates/partials/tasks-tab.html
index 04c20da..b506716 100644
--- a/web/templates/partials/tasks-tab.html
+++ b/web/templates/partials/tasks-tab.html
@@ -5,6 +5,40 @@
hx-target="#tab-content"
hx-swap="innerHTML">
+ <!-- Chains -->
+ {{if .Chains}}
+ <div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
+ {{range .Chains}}
+ <div class="bg-card bg-card-hover transition-colors rounded-lg border border-purple-400/20">
+ <div class="flex items-start gap-2 sm:gap-3 p-3 sm:p-4">
+ {{if .CurrentTask}}
+ <input type="checkbox"
+ hx-post="/complete-atom"
+ hx-vals='{"id": "{{.CurrentTask.ID}}", "source": "doot"}'
+ hx-target="closest div.rounded-lg"
+ hx-swap="outerHTML"
+ class="mt-1 h-5 w-5 rounded bg-black/40 border-white/30 text-white/80 focus:ring-white/30 cursor-pointer flex-shrink-0">
+ {{end}}
+ <span class="text-lg flex-shrink-0">🔗</span>
+ <div class="flex-1 min-w-0">
+ <div class="flex items-start justify-between gap-2">
+ <h3 class="text-sm text-white font-medium break-words cursor-pointer hover:underline"
+ hx-get="/chains/{{.Chain.ID}}"
+ hx-target="#chain-view-content"
+ hx-swap="innerHTML"
+ onclick="document.getElementById('chain-view-modal').classList.remove('hidden')">{{if .CurrentTask}}{{.CurrentTask.Content}}{{else}}{{.ProjectName}}{{end}}</h3>
+ </div>
+ <div class="flex flex-wrap items-center gap-2 mt-1 text-xs text-white/50">
+ <span class="text-purple-300/80">{{.ProjectName}} &middot; {{.Position}}/{{.Total}}</span>
+ {{if eq .Chain.Status "paused"}}<span class="text-amber-300/80">paused</span>{{end}}
+ </div>
+ </div>
+ </div>
+ </div>
+ {{end}}
+ </div>
+ {{end}}
+
<!-- Tasks List -->
{{if .Atoms}}
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@@ -35,6 +69,9 @@
{{end}}
</div>
<div class="flex flex-wrap items-center gap-2 mt-1 text-xs text-white/50">
+ {{if .ProjectName}}
+ <span class="text-teal-300/70">{{.ProjectName}}</span>
+ {{end}}
{{if .DueDate}}
<span class="{{if .IsOverdue}}text-amber-300/80{{end}}">{{.DueDate.Format "Jan 2"}}{{if .HasSetTime}}, {{.DueDate.Format "3:04pm"}}{{end}}</span>
{{end}}
@@ -92,6 +129,9 @@
{{end}}
</div>
<div class="flex flex-wrap items-center gap-2 mt-1 text-xs text-white/40">
+ {{if .ProjectName}}
+ <span class="text-teal-300/60">{{.ProjectName}}</span>
+ {{end}}
{{if .DueDate}}
<span>{{.DueDate.Format "Jan 2"}}{{if .HasSetTime}}, {{.DueDate.Format "3:04pm"}}{{end}}</span>
{{end}}