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
+1 -1
View File
@@ -246,7 +246,7 @@ def create_app() -> Quart:
# Start briefing scheduler
from fabledassistant.services.briefing_scheduler import start_briefing_scheduler
start_briefing_scheduler(asyncio.get_event_loop())
start_briefing_scheduler(asyncio.get_running_loop())
@app.after_serving
async def shutdown():
+2 -1
View File
@@ -78,7 +78,8 @@ async def quick_capture_route():
if not text:
return jsonify({"error": "text is required"}), 400
model = Config.OLLAMA_MODEL
from fabledassistant.services.settings import get_setting
model = await get_setting(uid, "default_model", Config.OLLAMA_MODEL)
# Build tool list for this user, then restrict to capture-only operations.
all_tools = await get_tools_for_user(uid)
@@ -274,6 +274,10 @@ async def run_compilation(user_id: int, slot: str, model: str | None = None) ->
),
)
if not internal_text and not external_text:
logger.warning("Briefing compilation produced no content for user %d slot %s", user_id, slot)
return ""
greeting = slot_greeting(slot)
today = internal_data["date"]
parts = [f"**{greeting}{today}**", ""]
-1
View File
@@ -132,7 +132,6 @@ Rules:
- create_event: appointments, meetings, scheduled occurrences with a date/time ("dentist Friday 2pm", "team meeting next Tuesday")
- update_note: updating, editing, appending to an existing note or task ("add to my shopping list: eggs", "mark buy milk done", "append to my meeting notes", "update my project note")
- research_topic: user wants a comprehensive research note from web sources ("research X", "look up X and make a note", "find everything about X", "compile a note on X")
- create_project: user wants to create a project, initiative, or campaign ("new project X", "start a project called Y", "create a project for Z")
- create_note: everything else — ideas, observations, links, excerpts, longer text
- For create_task / create_event: extract a concise title; put any extra detail in "body"
- For create_note: use a short descriptive title (≤60 chars); put the FULL original text as "body"
+32 -9
View File
@@ -530,16 +530,19 @@ _CORE_TOOLS = [
"function": {
"name": "get_weather",
"description": (
"Get the current 7-day weather forecast for the user's configured locations. "
"Returns temperature, precipitation, and conditions for each day. "
"Also reports any changes since the last forecast was fetched."
"Get the 7-day weather forecast. Pass a city/region name to look up any location, "
"or omit to get the user's configured home/work locations. "
"Returns temperature, precipitation, and conditions for each day."
),
"parameters": {
"type": "object",
"properties": {
"location_key": {
"location": {
"type": "string",
"description": "Which location to focus on: 'home', 'work', or 'all'. Defaults to 'all'.",
"description": (
"City, region, or country to look up — e.g. 'Portland, Oregon', 'Tokyo', 'home', 'work'. "
"Use 'home' or 'work' to get the user's saved locations. Omit for all saved locations."
),
}
},
"required": [],
@@ -1646,11 +1649,31 @@ async def execute_tool(user_id: int, tool_name: str, arguments: dict) -> dict:
return {"success": True, "log": log.to_dict(), "task": note.title}
elif tool_name == "get_weather":
from fabledassistant.services.weather import get_cached_weather
location_key = arguments.get("location_key", "all")
from fabledassistant.services.weather import (
get_cached_weather, geocode, _fetch_open_meteo, parse_forecast
)
from datetime import datetime, timezone as _tz
location = (arguments.get("location") or "").strip()
_known = {"home", "work", "all", ""}
if location.lower() not in _known:
# Live geocode + fetch for arbitrary city
try:
lat, lon, label = await geocode(location)
raw = await _fetch_open_meteo(lat, lon)
days = parse_forecast(raw)
return {"data": {"locations": [{
"location_key": "query",
"location_label": label,
"fetched_at": datetime.now(_tz.utc).isoformat(),
"days": days,
"changes_since_last_fetch": [],
}]}}
except Exception as e:
return {"success": False, "error": f"Could not get weather for '{location}': {e}"}
# Fall back to cached configured locations
locations = await get_cached_weather(user_id)
if location_key != "all":
locations = [loc for loc in locations if loc["location_key"] == location_key]
if location.lower() not in ("all", ""):
locations = [loc for loc in locations if loc["location_key"] == location.lower()]
return {"data": {"locations": locations}}
elif tool_name == "get_rss_items":