feat: make focus items permanent — no date scoping

Focus items now persist until explicitly removed. Removed date
filtering from main query, dedup subqueries, reorder, and sidebar
badge count. Removed date navigation from UI. The focus_date column
remains as metadata but no longer affects display.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 00:09:56 +00:00
parent 97027f2de4
commit 6aa36c570e
3 changed files with 25 additions and 45 deletions

View File

@@ -40,7 +40,7 @@ async def get_sidebar_data(db: AsyncSession) -> dict:
result = await db.execute(text(""" result = await db.execute(text("""
SELECT count(*) FROM daily_focus SELECT count(*) FROM daily_focus
WHERE is_deleted = false AND focus_date = CURRENT_DATE AND completed = false WHERE is_deleted = false AND completed = false
""")) """))
focus_count = result.scalar() or 0 focus_count = result.scalar() or 0

View File

@@ -1,4 +1,4 @@
"""Daily Focus: date-scoped task/list-item commitment list.""" """Focus: persistent commitment list — items stay until removed."""
from fastapi import APIRouter, Request, Form, Depends from fastapi import APIRouter, Request, Form, Depends
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -21,7 +21,6 @@ templates.env.filters["autolink"] = autolink
@router.get("/") @router.get("/")
async def focus_view( async def focus_view(
request: Request, request: Request,
focus_date: Optional[str] = None,
domain_id: Optional[str] = None, domain_id: Optional[str] = None,
area_id: Optional[str] = None, area_id: Optional[str] = None,
project_id: Optional[str] = None, project_id: Optional[str] = None,
@@ -30,11 +29,10 @@ async def focus_view(
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
sidebar = await get_sidebar_data(db) sidebar = await get_sidebar_data(db)
target_date = date.fromisoformat(focus_date) if focus_date else date.today()
if not source_type: if not source_type:
source_type = "tasks" source_type = "tasks"
# --- Focus items (both tasks and list items) --- # --- All active focus items ---
result = await db.execute(text(""" result = await db.execute(text("""
SELECT df.*, SELECT df.*,
t.title as title, t.priority, t.status as task_status, t.title as title, t.priority, t.status as task_status,
@@ -60,11 +58,11 @@ async def focus_view(
LEFT JOIN projects lp ON l.project_id = lp.id LEFT JOIN projects lp ON l.project_id = lp.id
LEFT JOIN domains ld ON l.domain_id = ld.id LEFT JOIN domains ld ON l.domain_id = ld.id
LEFT JOIN areas la ON l.area_id = la.id LEFT JOIN areas la ON l.area_id = la.id
WHERE df.focus_date = :target_date AND df.is_deleted = false WHERE df.is_deleted = false
AND (t.id IS NULL OR t.is_deleted = false) AND (t.id IS NULL OR t.is_deleted = false)
AND (li.id IS NULL OR li.is_deleted = false) AND (li.id IS NULL OR li.is_deleted = false)
ORDER BY df.sort_order, df.created_at ORDER BY df.sort_order, df.created_at
"""), {"target_date": target_date}) """))
items = [dict(r._mapping) for r in result] items = [dict(r._mapping) for r in result]
# Group items by domain only — area/project shown as inline columns # Group items by domain only — area/project shown as inline columns
@@ -86,9 +84,9 @@ async def focus_view(
avail_where = [ avail_where = [
"t.is_deleted = false", "t.is_deleted = false",
"t.status NOT IN ('done', 'cancelled')", "t.status NOT IN ('done', 'cancelled')",
"t.id NOT IN (SELECT task_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false AND task_id IS NOT NULL)", "t.id NOT IN (SELECT task_id FROM daily_focus WHERE is_deleted = false AND task_id IS NOT NULL)",
] ]
avail_params = {"target_date": target_date} avail_params = {}
if search: if search:
avail_where.append("t.title ILIKE :search") avail_where.append("t.title ILIKE :search")
@@ -123,9 +121,9 @@ async def focus_view(
"li.is_deleted = false", "li.is_deleted = false",
"li.completed = false", "li.completed = false",
"l.is_deleted = false", "l.is_deleted = false",
"li.id NOT IN (SELECT list_item_id FROM daily_focus WHERE focus_date = :target_date AND is_deleted = false AND list_item_id IS NOT NULL)", "li.id NOT IN (SELECT list_item_id FROM daily_focus WHERE is_deleted = false AND list_item_id IS NOT NULL)",
] ]
li_params = {"target_date": target_date} li_params = {}
if search: if search:
li_where.append("li.content ILIKE :search") li_where.append("li.content ILIKE :search")
@@ -169,7 +167,6 @@ async def focus_view(
"items": items, "hierarchy": hierarchy, "items": items, "hierarchy": hierarchy,
"available_tasks": available_tasks, "available_tasks": available_tasks,
"available_list_items": available_list_items, "available_list_items": available_list_items,
"focus_date": target_date,
"total_estimated": total_est, "total_estimated": total_est,
"domains": domains, "areas": areas, "projects": projects, "domains": domains, "areas": areas, "projects": projects,
"current_domain_id": domain_id or "", "current_domain_id": domain_id or "",
@@ -177,29 +174,27 @@ async def focus_view(
"current_project_id": project_id or "", "current_project_id": project_id or "",
"current_search": search or "", "current_search": search or "",
"current_source_type": source_type, "current_source_type": source_type,
"page_title": "Daily Focus", "active_nav": "focus", "page_title": "Focus", "active_nav": "focus",
}) })
@router.post("/add") @router.post("/add")
async def add_to_focus( async def add_to_focus(
request: Request, request: Request,
focus_date: str = Form(...),
task_id: Optional[str] = Form(None), task_id: Optional[str] = Form(None),
list_item_id: Optional[str] = Form(None), list_item_id: Optional[str] = Form(None),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
repo = BaseRepository("daily_focus", db) repo = BaseRepository("daily_focus", db)
parsed_date = date.fromisoformat(focus_date)
# Get next sort order # Get next sort order
result = await db.execute(text(""" result = await db.execute(text("""
SELECT COALESCE(MAX(sort_order), 0) + 10 FROM daily_focus SELECT COALESCE(MAX(sort_order), 0) + 10 FROM daily_focus
WHERE focus_date = :fd AND is_deleted = false WHERE is_deleted = false
"""), {"fd": parsed_date}) """))
next_order = result.scalar() next_order = result.scalar()
data = { data = {
"focus_date": parsed_date, "focus_date": date.today(),
"sort_order": next_order, "sort_order": next_order,
"completed": False, "completed": False,
} }
@@ -209,7 +204,7 @@ async def add_to_focus(
data["list_item_id"] = list_item_id data["list_item_id"] = list_item_id
await repo.create(data) await repo.create(data)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) return RedirectResponse(url="/focus", status_code=303)
@router.post("/reorder") @router.post("/reorder")
@@ -217,27 +212,24 @@ async def reorder_focus(
request: Request, request: Request,
item_id: str = Form(...), item_id: str = Form(...),
direction: str = Form(...), direction: str = Form(...),
focus_date: str = Form(...),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
repo = BaseRepository("daily_focus", db) repo = BaseRepository("daily_focus", db)
parsed_date = date.fromisoformat(focus_date) await repo.move_in_order(item_id, direction)
await repo.move_in_order(item_id, direction, filters={"focus_date": parsed_date}) return RedirectResponse(url="/focus", status_code=303)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@router.post("/reorder-all") @router.post("/reorder-all")
async def reorder_all_focus( async def reorder_all_focus(
request: Request, request: Request,
item_ids: str = Form(...), item_ids: str = Form(...),
focus_date: str = Form(...),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
repo = BaseRepository("daily_focus", db) repo = BaseRepository("daily_focus", db)
ids = [i.strip() for i in item_ids.split(",") if i.strip()] ids = [i.strip() for i in item_ids.split(",") if i.strip()]
if ids: if ids:
await repo.reorder(ids) await repo.reorder(ids)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303) return RedirectResponse(url="/focus", status_code=303)
@router.post("/{focus_id}/toggle") @router.post("/{focus_id}/toggle")
@@ -265,14 +257,11 @@ async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depen
"UPDATE list_items SET completed = false, completed_at = NULL, updated_at = :now WHERE id = :id" "UPDATE list_items SET completed = false, completed_at = NULL, updated_at = :now WHERE id = :id"
), {"id": item["list_item_id"], "now": now}) ), {"id": item["list_item_id"], "now": now})
await db.commit() await db.commit()
focus_date = item["focus_date"] if item else date.today() return RedirectResponse(url="/focus", status_code=303)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@router.post("/{focus_id}/remove") @router.post("/{focus_id}/remove")
async def remove_from_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)): async def remove_from_focus(focus_id: str, request: Request, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("daily_focus", db) repo = BaseRepository("daily_focus", db)
item = await repo.get(focus_id)
await repo.soft_delete(focus_id) await repo.soft_delete(focus_id)
focus_date = item["focus_date"] if item else date.today() return RedirectResponse(url="/focus", status_code=303)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)

View File

@@ -1,17 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Daily Focus</h1> <h1 class="page-title">Focus</h1>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{% if total_estimated %}<span class="text-sm text-muted">~{{ total_estimated }}min estimated</span>{% endif %} {% if total_estimated %}<span class="text-sm text-muted">~{{ total_estimated }}min estimated</span>{% endif %}
</div> </div>
</div> </div>
<div class="flex items-center gap-2 mb-4">
<a href="/focus?focus_date={{ (focus_date|string)[:10] }}" class="btn btn-ghost btn-sm">Today</a>
<span class="text-sm" style="font-weight:600">{{ focus_date }}</span>
</div>
<!-- Focus items grouped by domain --> <!-- Focus items grouped by domain -->
{% if items %} {% if items %}
<div class="card"> <div class="card">
@@ -37,7 +32,7 @@
{% for item in domain.rows %} {% for item in domain.rows %}
<tr class="focus-drag-row" draggable="true" data-id="{{ item.id }}" style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}"> <tr class="focus-drag-row" draggable="true" data-id="{{ item.id }}" style="border-bottom:1px solid var(--border);{{ 'opacity:0.6;' if item.completed }}">
<td class="focus-row-num" style="padding:1px 2px;vertical-align:middle;text-align:center;color:var(--muted);font-size:0.72rem;font-weight:600;">{{ loop.index }}</td> <td class="focus-row-num" style="padding:1px 2px;vertical-align:middle;text-align:center;color:var(--muted);font-size:0.72rem;font-weight:600;">{{ loop.index }}</td>
<td style="padding:1px 1px;vertical-align:middle;">{% with reorder_url="/focus/reorder", item_id=item.id, extra_fields={"focus_date": focus_date|string} %}{% include 'partials/reorder_arrows.html' %}{% endwith %}</td> <td style="padding:1px 1px;vertical-align:middle;">{% with reorder_url="/focus/reorder", item_id=item.id %}{% include 'partials/reorder_arrows.html' %}{% endwith %}</td>
<td style="padding:1px 1px;vertical-align:middle;"> <td style="padding:1px 1px;vertical-align:middle;">
<form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline"> <form action="/focus/{{ item.id }}/toggle" method="post" style="display:inline">
<div class="row-check"><input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()"><label for="f-{{ item.id }}"></label></div> <div class="row-check"><input type="checkbox" id="f-{{ item.id }}" {{ 'checked' if item.completed }} onchange="this.form.submit()"><label for="f-{{ item.id }}"></label></div>
@@ -61,11 +56,10 @@
</table> </table>
<form id="focus-reorder-form" action="/focus/reorder-all" method="post" style="display:none;"> <form id="focus-reorder-form" action="/focus/reorder-all" method="post" style="display:none;">
<input type="hidden" name="item_ids" id="focus-reorder-ids"> <input type="hidden" name="item_ids" id="focus-reorder-ids">
<input type="hidden" name="focus_date" value="{{ focus_date }}">
</form> </form>
</div> </div>
{% else %} {% else %}
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div> <div class="empty-state mb-4"><div class="empty-state-text">No focus items yet</div></div>
{% endif %} {% endif %}
<!-- Add to Focus --> <!-- Add to Focus -->
@@ -74,15 +68,14 @@
<!-- Tab strip --> <!-- Tab strip -->
<div class="tab-strip" style="padding: 0 10px;"> <div class="tab-strip" style="padding: 0 10px;">
<a href="/focus?focus_date={{ focus_date }}&source_type=tasks{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}" <a href="/focus?source_type=tasks{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
class="tab-item {{ 'active' if current_source_type == 'tasks' }}">Tasks</a> class="tab-item {{ 'active' if current_source_type == 'tasks' }}">Tasks</a>
<a href="/focus?focus_date={{ focus_date }}&source_type=list_items{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}" <a href="/focus?source_type=list_items{% if current_search %}&search={{ current_search }}{% endif %}{% if current_domain_id %}&domain_id={{ current_domain_id }}{% endif %}{% if current_project_id %}&project_id={{ current_project_id }}{% endif %}"
class="tab-item {{ 'active' if current_source_type == 'list_items' }}">List Items</a> class="tab-item {{ 'active' if current_source_type == 'list_items' }}">List Items</a>
</div> </div>
<!-- Filters --> <!-- Filters -->
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 4px 10px; border-bottom: 1px solid var(--border);"> <form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 4px 10px; border-bottom: 1px solid var(--border);">
<input type="hidden" name="focus_date" value="{{ focus_date }}">
<input type="hidden" name="source_type" value="{{ current_source_type }}"> <input type="hidden" name="source_type" value="{{ current_source_type }}">
<input type="text" name="search" value="{{ current_search }}" placeholder="Search..." class="filter-select" style="min-width:150px"> <input type="text" name="search" value="{{ current_search }}" placeholder="Search..." class="filter-select" style="min-width:150px">
<select name="domain_id" class="filter-select" id="focus-domain" onchange="this.form.submit()"> <select name="domain_id" class="filter-select" id="focus-domain" onchange="this.form.submit()">
@@ -107,7 +100,7 @@
</select> </select>
<button type="submit" class="btn btn-ghost btn-xs">Search</button> <button type="submit" class="btn btn-ghost btn-xs">Search</button>
{% if current_search or current_domain_id or current_area_id or current_project_id %} {% if current_search or current_domain_id or current_area_id or current_project_id %}
<a href="/focus?focus_date={{ focus_date }}&source_type={{ current_source_type }}" class="btn btn-ghost btn-xs" style="color:var(--red)">Clear</a> <a href="/focus?source_type={{ current_source_type }}" class="btn btn-ghost btn-xs" style="color:var(--red)">Clear</a>
{% endif %} {% endif %}
</form> </form>
@@ -121,7 +114,6 @@
{% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %} {% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %}
<form action="/focus/add" method="post" style="display:inline"> <form action="/focus/add" method="post" style="display:inline">
<input type="hidden" name="task_id" value="{{ t.id }}"> <input type="hidden" name="task_id" value="{{ t.id }}">
<input type="hidden" name="focus_date" value="{{ focus_date }}">
<button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button> <button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button>
</form> </form>
</div> </div>
@@ -143,7 +135,6 @@
{% if li.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ li.list_name }}</span>{% endif %} {% if li.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ li.list_name }}</span>{% endif %}
<form action="/focus/add" method="post" style="display:inline"> <form action="/focus/add" method="post" style="display:inline">
<input type="hidden" name="list_item_id" value="{{ li.id }}"> <input type="hidden" name="list_item_id" value="{{ li.id }}">
<input type="hidden" name="focus_date" value="{{ focus_date }}">
<button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button> <button class="btn btn-ghost btn-xs" style="color:var(--green)">+ Focus</button>
</form> </form>
</div> </div>