feat: kanban status buttons, task back-nav, RSS UI, weather search, briefing fixes

Project view:
- Add inline status advance buttons on kanban task cards (todo→in_progress,
  in_progress→done); buttons reveal on hover, stop link navigation

Task viewer:
- Back button navigates to task's project instead of /tasks when project_id set
- Esc key navigates to project (or /tasks); blurs focused element first

Quick capture:
- Use user's configured model instead of hardcoded Config.OLLAMA_MODEL
- Remove create_project from classifier prompt (tool not offered, caused
  task-shaped inputs to silently fall through to note fallback)

Briefing scheduler:
- Fix get_event_loop() → get_running_loop() so background thread uses the
  correct hypercorn event loop (jobs were scheduling but never executing)
- Suppress bare greeting when both LLM synthesis lanes return empty

RSS feed UI (SettingsView):
- Show last-fetched age, category badge, and feed URL per row
- Category input field when adding a feed
- Refresh all button: fetches latest items, reloads list, toasts with count
- Enter key submits add-feed form; better empty-state hint with example feeds

Weather tool:
- Accept any city/region name in addition to 'home'/'work'/'all'
- Geocodes via Nominatim + fetches live from Open-Meteo for arbitrary queries

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 00:42:01 -04:00
parent a9414cf949
commit a2ba90160c
9 changed files with 238 additions and 34 deletions
+65 -6
View File
@@ -209,6 +209,30 @@ async function loadTasks() {
}
}
const advancingTaskId = ref<number | null>(null);
const taskStatusNext: Record<string, string> = {
todo: "in_progress",
in_progress: "done",
};
async function advanceTaskStatus(task: NoteItem, e: Event) {
e.preventDefault();
e.stopPropagation();
const next = taskStatusNext[task.status ?? ""];
if (!next || advancingTaskId.value === task.id) return;
advancingTaskId.value = task.id;
try {
await apiPatch(`/api/notes/${task.id}`, { status: next });
const idx = tasks.value.findIndex((t) => t.id === task.id);
if (idx !== -1) tasks.value[idx] = { ...tasks.value[idx], status: next };
} catch {
toast.show("Failed to update task", "error");
} finally {
advancingTaskId.value = null;
}
}
async function loadNotes() {
notesLoading.value = true;
try {
@@ -475,9 +499,17 @@ async function confirmDelete() {
:class="['task-card', `pri-${task.priority || 'none'}`]"
>
<span class="task-title">{{ task.title || "Untitled" }}</span>
<div v-if="task.priority !== 'none' || task.due_date" class="task-meta">
<span v-if="task.priority && task.priority !== 'none'" :class="['priority-dot', `dot-pri-${task.priority}`]" :title="task.priority"></span>
<span v-if="task.due_date" class="due-date">{{ task.due_date }}</span>
<div class="task-card-footer">
<div v-if="task.priority !== 'none' || task.due_date" class="task-meta">
<span v-if="task.priority && task.priority !== 'none'" :class="['priority-dot', `dot-pri-${task.priority}`]" :title="task.priority"></span>
<span v-if="task.due_date" class="due-date">{{ task.due_date }}</span>
</div>
<button
class="task-advance-btn"
title="Move to In Progress"
:disabled="advancingTaskId === task.id"
@click="advanceTaskStatus(task, $event)"
></button>
</div>
</router-link>
<p v-if="!group.tasks.filter(t => t.status === 'todo').length" class="col-empty">No tasks</p>
@@ -498,9 +530,17 @@ async function confirmDelete() {
:class="['task-card', `pri-${task.priority || 'none'}`]"
>
<span class="task-title">{{ task.title || "Untitled" }}</span>
<div v-if="task.priority !== 'none' || task.due_date" class="task-meta">
<span v-if="task.priority && task.priority !== 'none'" :class="['priority-dot', `dot-pri-${task.priority}`]" :title="task.priority"></span>
<span v-if="task.due_date" class="due-date">{{ task.due_date }}</span>
<div class="task-card-footer">
<div v-if="task.priority !== 'none' || task.due_date" class="task-meta">
<span v-if="task.priority && task.priority !== 'none'" :class="['priority-dot', `dot-pri-${task.priority}`]" :title="task.priority"></span>
<span v-if="task.due_date" class="due-date">{{ task.due_date }}</span>
</div>
<button
class="task-advance-btn task-advance-btn--done"
title="Mark as Done"
:disabled="advancingTaskId === task.id"
@click="advanceTaskStatus(task, $event)"
></button>
</div>
</router-link>
<p v-if="!group.tasks.filter(t => t.status === 'in_progress').length" class="col-empty">No tasks</p>
@@ -1088,7 +1128,26 @@ async function confirmDelete() {
.task-card-done .task-title { text-decoration: line-through; }
.task-title { display: block; font-weight: 500; margin-bottom: 0.2rem; line-height: 1.3; word-break: break-word; }
.task-card-footer { display: flex; align-items: center; justify-content: space-between; gap: 0.35rem; min-height: 1.2rem; }
.task-meta { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; }
.task-advance-btn {
flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
width: 1.4rem; height: 1.4rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: transparent;
color: var(--color-text-muted);
font-size: 0.75rem;
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s, color 0.15s;
line-height: 1;
}
.task-card:hover .task-advance-btn { opacity: 1; }
.task-advance-btn:hover { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
.task-advance-btn--done:hover { background: var(--color-success, #22c55e); border-color: var(--color-success, #22c55e); color: #fff; }
.task-advance-btn:disabled { opacity: 0.4; cursor: default; }
.priority-dot {
width: 7px;