Workspace polish: milestone selector, streaming fix, push fix, log quieting
- WorkspaceTaskPanel: add milestone <select> in task detail (PATCH /api/notes/:id), replace delete ✕ with trash can SVG icon - WorkspaceView: scroll to bottom when streaming ends so final message is visible without a page refresh - ToolCallCard: fix search_notes result count (was reading data.total; tool returns data.count), so results no longer show "0 found" - push.py: switch from deprecated WebPusher().send(vapid_private_key=...) to webpush() function (pywebpush 2.x API compatibility) - app.py: downgrade /api/health, /api/chat/status, and static asset requests from INFO to DEBUG in after_request logger to reduce log noise at default log level Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -262,7 +262,7 @@ async function applyTag(tag: string) {
|
||||
<span v-else class="tool-search-info">No notes found</span>
|
||||
</template>
|
||||
<template v-else-if="searchResults">
|
||||
<span class="tool-search-info">{{ (toolCall.result.data?.total as number) ?? 0 }} found</span>
|
||||
<span class="tool-search-info">{{ (toolCall.result.data?.count as number) ?? (toolCall.result.data?.total as number) ?? 0 }} found</span>
|
||||
<div class="tool-search-results">
|
||||
<router-link
|
||||
v-for="r in searchResults"
|
||||
|
||||
@@ -127,6 +127,23 @@ async function addTask() {
|
||||
|
||||
const deletingTask = ref(false);
|
||||
const deleteConfirmPending = ref(false);
|
||||
const changingMilestone = ref(false);
|
||||
|
||||
async function setMilestone(milestoneId: number | null) {
|
||||
if (!activeTask.value) return;
|
||||
changingMilestone.value = true;
|
||||
try {
|
||||
await apiPatch(`/api/notes/${activeTask.value.id}`, { milestone_id: milestoneId });
|
||||
activeTask.value.milestone_id = milestoneId;
|
||||
// Update in tasks list too
|
||||
const t = tasks.value.find((x) => x.id === activeTask.value!.id);
|
||||
if (t) t.milestone_id = milestoneId;
|
||||
} catch {
|
||||
toast.show("Failed to update milestone", "error");
|
||||
} finally {
|
||||
changingMilestone.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteActiveTask() {
|
||||
if (!activeTask.value) return;
|
||||
@@ -192,7 +209,7 @@ defineExpose({ reload: loadAll });
|
||||
<button class="btn-delete-cancel" @click="cancelDeleteTask">✕</button>
|
||||
</template>
|
||||
<button v-else class="btn-delete-task" title="Delete task" @click="deleteActiveTask">
|
||||
✕
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -205,6 +222,15 @@ defineExpose({ reload: loadAll });
|
||||
<span v-if="activeTask.due_date" class="meta-chip due">
|
||||
Due {{ activeTask.due_date }}
|
||||
</span>
|
||||
<select
|
||||
class="milestone-select"
|
||||
:value="activeTask.milestone_id ?? ''"
|
||||
:disabled="changingMilestone"
|
||||
@change="setMilestone(($event.target as HTMLSelectElement).value === '' ? null : Number(($event.target as HTMLSelectElement).value))"
|
||||
>
|
||||
<option value="">No Milestone</option>
|
||||
<option v-for="ms in milestones" :key="ms.id" :value="ms.id">{{ ms.title }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="detail-log">
|
||||
@@ -562,6 +588,19 @@ defineExpose({ reload: loadAll });
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.milestone-select {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 10px;
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
max-width: 140px;
|
||||
}
|
||||
.milestone-select:disabled { opacity: 0.5; cursor: default; }
|
||||
.milestone-select:focus { outline: none; border-color: var(--color-primary); }
|
||||
|
||||
.detail-log {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
@@ -79,7 +79,10 @@ watch(
|
||||
watch(
|
||||
() => chatStore.streaming,
|
||||
(s) => {
|
||||
if (!s) processedCount.value = 0;
|
||||
if (!s) {
|
||||
processedCount.value = 0;
|
||||
scrollToBottom();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -74,7 +74,13 @@ def create_app() -> Quart:
|
||||
async def after_request(response):
|
||||
duration = time.monotonic() - getattr(g, "request_start", time.monotonic())
|
||||
duration_ms = round(duration * 1000, 1)
|
||||
logger.info(
|
||||
# Downgrade noisy high-frequency / static paths to DEBUG
|
||||
_quiet = (
|
||||
request.path in {"/api/health", "/api/chat/status"}
|
||||
or request.path.startswith(("/static/", "/assets/", "/sw.js", "/manifest.json"))
|
||||
)
|
||||
log_fn = logger.debug if _quiet else logger.info
|
||||
log_fn(
|
||||
"%s %s %s %.1fms",
|
||||
request.method,
|
||||
request.path,
|
||||
|
||||
@@ -23,8 +23,8 @@ _VAPID_KEYS_FILE = Path(Config.IMAGE_CACHE_DIR).parent / "vapid_keys.json"
|
||||
def _get_webpush():
|
||||
"""Lazy import to avoid startup errors if pywebpush is not installed."""
|
||||
try:
|
||||
from pywebpush import WebPusher
|
||||
return WebPusher
|
||||
from pywebpush import webpush
|
||||
return webpush
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
@@ -156,8 +156,8 @@ async def send_push_notification(
|
||||
logger.debug("VAPID not configured, skipping push notification")
|
||||
return
|
||||
|
||||
WebPusher = _get_webpush()
|
||||
if WebPusher is None:
|
||||
webpush = _get_webpush()
|
||||
if webpush is None:
|
||||
logger.warning("pywebpush not installed, cannot send push notifications")
|
||||
return
|
||||
|
||||
@@ -182,7 +182,8 @@ async def send_push_notification(
|
||||
"endpoint": sub.endpoint,
|
||||
"keys": {"p256dh": sub.p256dh, "auth": sub.auth},
|
||||
}
|
||||
response = WebPusher(subscription_info).send(
|
||||
response = webpush(
|
||||
subscription_info=subscription_info,
|
||||
data=payload,
|
||||
vapid_private_key=Config.VAPID_PRIVATE_KEY,
|
||||
vapid_claims=vapid_claims,
|
||||
|
||||
Reference in New Issue
Block a user