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>
|
<span v-else class="tool-search-info">No notes found</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="searchResults">
|
<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">
|
<div class="tool-search-results">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="r in searchResults"
|
v-for="r in searchResults"
|
||||||
|
|||||||
@@ -127,6 +127,23 @@ async function addTask() {
|
|||||||
|
|
||||||
const deletingTask = ref(false);
|
const deletingTask = ref(false);
|
||||||
const deleteConfirmPending = 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() {
|
async function deleteActiveTask() {
|
||||||
if (!activeTask.value) return;
|
if (!activeTask.value) return;
|
||||||
@@ -192,7 +209,7 @@ defineExpose({ reload: loadAll });
|
|||||||
<button class="btn-delete-cancel" @click="cancelDeleteTask">✕</button>
|
<button class="btn-delete-cancel" @click="cancelDeleteTask">✕</button>
|
||||||
</template>
|
</template>
|
||||||
<button v-else class="btn-delete-task" title="Delete task" @click="deleteActiveTask">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -205,6 +222,15 @@ defineExpose({ reload: loadAll });
|
|||||||
<span v-if="activeTask.due_date" class="meta-chip due">
|
<span v-if="activeTask.due_date" class="meta-chip due">
|
||||||
Due {{ activeTask.due_date }}
|
Due {{ activeTask.due_date }}
|
||||||
</span>
|
</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>
|
||||||
|
|
||||||
<div class="detail-log">
|
<div class="detail-log">
|
||||||
@@ -562,6 +588,19 @@ defineExpose({ reload: loadAll });
|
|||||||
text-transform: capitalize;
|
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 {
|
.detail-log {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
@@ -79,7 +79,10 @@ watch(
|
|||||||
watch(
|
watch(
|
||||||
() => chatStore.streaming,
|
() => chatStore.streaming,
|
||||||
(s) => {
|
(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):
|
async def after_request(response):
|
||||||
duration = time.monotonic() - getattr(g, "request_start", time.monotonic())
|
duration = time.monotonic() - getattr(g, "request_start", time.monotonic())
|
||||||
duration_ms = round(duration * 1000, 1)
|
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",
|
"%s %s %s %.1fms",
|
||||||
request.method,
|
request.method,
|
||||||
request.path,
|
request.path,
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ _VAPID_KEYS_FILE = Path(Config.IMAGE_CACHE_DIR).parent / "vapid_keys.json"
|
|||||||
def _get_webpush():
|
def _get_webpush():
|
||||||
"""Lazy import to avoid startup errors if pywebpush is not installed."""
|
"""Lazy import to avoid startup errors if pywebpush is not installed."""
|
||||||
try:
|
try:
|
||||||
from pywebpush import WebPusher
|
from pywebpush import webpush
|
||||||
return WebPusher
|
return webpush
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -156,8 +156,8 @@ async def send_push_notification(
|
|||||||
logger.debug("VAPID not configured, skipping push notification")
|
logger.debug("VAPID not configured, skipping push notification")
|
||||||
return
|
return
|
||||||
|
|
||||||
WebPusher = _get_webpush()
|
webpush = _get_webpush()
|
||||||
if WebPusher is None:
|
if webpush is None:
|
||||||
logger.warning("pywebpush not installed, cannot send push notifications")
|
logger.warning("pywebpush not installed, cannot send push notifications")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -182,7 +182,8 @@ async def send_push_notification(
|
|||||||
"endpoint": sub.endpoint,
|
"endpoint": sub.endpoint,
|
||||||
"keys": {"p256dh": sub.p256dh, "auth": sub.auth},
|
"keys": {"p256dh": sub.p256dh, "auth": sub.auth},
|
||||||
}
|
}
|
||||||
response = WebPusher(subscription_info).send(
|
response = webpush(
|
||||||
|
subscription_info=subscription_info,
|
||||||
data=payload,
|
data=payload,
|
||||||
vapid_private_key=Config.VAPID_PRIVATE_KEY,
|
vapid_private_key=Config.VAPID_PRIVATE_KEY,
|
||||||
vapid_claims=vapid_claims,
|
vapid_claims=vapid_claims,
|
||||||
|
|||||||
Reference in New Issue
Block a user