Compare commits

..

22 Commits

Author SHA1 Message Date
7a2c6d3f2a feat: sequential numbering on focus items within domain groups
Display-only row numbers (1, 2, 3...) scoped per domain group.
Numbers update instantly on drag-and-drop reorder via JS renumber.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:12:19 +00:00
1bb92ea87d fix: filter deleted tasks/list items from focus, redirect task delete to list
Focus query now excludes soft-deleted tasks and list items.
Task delete redirects to /tasks instead of back to the (now deleted)
task's edit page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:08:35 +00:00
b5c8f305dd feat: add delete button to task detail and edit pages
Task detail page gets a Delete button in the header bar (matching
all other entity detail pages). Task edit form gets a Delete button
below the form. Both use confirmation dialog via data-confirm.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:04:45 +00:00
9833f131e1 feat: collapsible domain groups on focus page
Domain headers are now clickable to expand/collapse their item rows,
with a chevron indicator and item count badge. State persists via
localStorage, matching the sidebar toggle pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 16:00:01 +00:00
1a6b3fac1d feat: drag-and-drop reorder for daily focus items
Add HTML5 drag-and-drop within domain groups on the focus page.
Items can be dragged to reorder within their domain; cross-domain
drag is prevented. Uses hidden form POST (no fetch/XHR). Arrow
buttons kept as fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:58:05 +00:00
4c072beec0 feat: focus page table layout with domain grouping, area/project columns, compact padding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:51:14 +00:00
bbb80067ef fix: align focus item columns with fixed-width due date placeholder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:35:17 +00:00
997ea786ba fix: sort focus projects — General first, then alphabetical
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:29:02 +00:00
4c51a1daad fix: show "General" headers for items without area/project in focus hierarchy
Items without a project or area were missing group headers, causing them
to appear under the previous project's group visually.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:26:42 +00:00
73be08e7cc fix: use muted gray for project headers in focus hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:23:28 +00:00
d034f4af4e fix: use accent blue for project headers in focus hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:22:00 +00:00
f6a9b86131 fix: project header text color to white in focus hierarchy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:19:15 +00:00
ec2bd51585 feat: domain > area > project hierarchy for daily focus with compact padding
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:11:36 +00:00
50200c23cc feat: group daily focus items by project with "General" fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:06:58 +00:00
0be6566045 feat: add text search to All Tasks page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 15:02:23 +00:00
2094ea5fbe feat: daily focus search, show all tasks, and list item support
- Add list_item_id column to daily_focus (task_id now nullable, CHECK constraint ensures exactly one)
- Remove LIMIT 50 + [:15] slice — show up to 200 items with "show more" at 25
- Add text search (ILIKE) for filtering available items
- Add tab strip to switch between Tasks and List Items sources
- Toggle syncs list_item completed status alongside task status
- Graceful [Deleted] fallback for removed source items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:52:41 +00:00
f88c6e5fd4 fix: notes preview text wrapping and right margin
Add word-break/overflow-wrap to .detail-body so long text wraps
properly, and add right margin inside the preview card for balanced
spacing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:27:49 +00:00
c42fe9dc13 fix: consistent row styling for capture and focus items
- Remove card-style spacing (margin, border-radius, background) from capture-item and focus-item
- Use border-bottom pattern matching list-row for uniform density
- Wrap capture and focus items in .card containers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:57:40 +00:00
34b232de5f feat: consistent compact density across all list views
- Reduce capture-item, files table, date group labels to 6px 12px padding
- Set font-size 0.80rem on capture-text, file table cells

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:53:41 +00:00
497436a0a3 feat: universal reorder grip handles and compact UI density
- Add generic move_in_order() to BaseRepository for reorder support
- Add reusable reorder_arrows.html partial with grip dot handles
- Add reorder routes to all 9 list routers (tasks, notes, links, contacts, meetings, decisions, appointments, lists, focus)
- Compact row padding (6px 12px, gap 8px) on .list-row and .focus-item
- Reduce font size to 0.80rem on row titles, sidebar nav, domain tree

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:44:30 +00:00
75b055299a feat: file search, type/tag filters, and pagination
Add tsvector full-text search, type filter (image/document/text/
spreadsheet/archive), tag filter dropdown, and pagination (50/page).
Replace folder button bar with compact dropdown. All filters combine
and carry through sort and pagination links.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 01:05:10 +00:00
41b974c804 feat: sortable file table and dropdown folder picker
Replace flat folder button bar with compact dropdown select.
Add sortable columns (path, name, date) to file list table.
Restore soft-deleted files on sync when file still on disk.
Serve text file previews with white background.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:49:16 +00:00
29 changed files with 997 additions and 186 deletions

View File

@@ -161,7 +161,7 @@ class BaseRepository:
"category", "instructions", "expected_output", "estimated_days",
"contact_id", "started_at",
"weekly_hours", "effective_from",
"task_id", "meeting_id",
"task_id", "meeting_id", "list_item_id",
}
clean_data = {}
for k, v in data.items():
@@ -248,3 +248,72 @@ class BaseRepository:
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": (i + 1) * 10, "id": str(id)}
)
async def swap_sort_order(self, id_a: str, id_b: str) -> None:
"""Swap sort_order between two rows."""
result = await self.db.execute(
text(f"SELECT id, sort_order FROM {self.table} WHERE id IN (:a, :b)"),
{"a": str(id_a), "b": str(id_b)},
)
rows = {str(r._mapping["id"]): r._mapping["sort_order"] for r in result}
if len(rows) != 2:
return
await self.db.execute(
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": rows[str(id_b)], "id": str(id_a)},
)
await self.db.execute(
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": rows[str(id_a)], "id": str(id_b)},
)
async def move_in_order(self, item_id: str, direction: str, filters: dict | None = None) -> None:
"""Move an item up or down within its sort group.
Handles lazy initialization (all sort_order=0) and swaps with neighbor.
filters: optional dict to scope the group (e.g. {"list_id": some_id}).
"""
where_clauses = ["is_deleted = false"]
params: dict[str, Any] = {}
if filters:
for i, (key, value) in enumerate(filters.items()):
if value is None:
where_clauses.append(f"{key} IS NULL")
else:
param_name = f"mf_{i}"
where_clauses.append(f"{key} = :{param_name}")
params[param_name] = value
where_sql = " AND ".join(where_clauses)
result = await self.db.execute(
text(f"SELECT id, sort_order FROM {self.table} WHERE {where_sql} ORDER BY sort_order, created_at"),
params,
)
items = [dict(r._mapping) for r in result]
if len(items) < 2:
return
# Lazy init: if all sort_order are 0, assign incremental values
if all(r["sort_order"] == 0 for r in items):
for i, r in enumerate(items):
await self.db.execute(
text(f"UPDATE {self.table} SET sort_order = :order WHERE id = :id"),
{"order": (i + 1) * 10, "id": str(r["id"])},
)
# Re-fetch
result = await self.db.execute(
text(f"SELECT id, sort_order FROM {self.table} WHERE {where_sql} ORDER BY sort_order, created_at"),
params,
)
items = [dict(r._mapping) for r in result]
ids = [str(r["id"]) for r in items]
if item_id not in ids:
return
idx = ids.index(item_id)
if direction == "up" and idx > 0:
await self.swap_sort_order(ids[idx], ids[idx - 1])
elif direction == "down" and idx < len(ids) - 1:
await self.swap_sort_order(ids[idx], ids[idx + 1])

View File

@@ -300,3 +300,15 @@ async def delete_appointment(
repo = BaseRepository("appointments", db)
await repo.soft_delete(appointment_id)
return RedirectResponse(url="/appointments", status_code=303)
@router.post("/reorder")
async def reorder_appointment(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("appointments", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/appointments"), status_code=303)

View File

@@ -124,3 +124,15 @@ async def delete_contact(contact_id: str, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("contacts", db)
await repo.soft_delete(contact_id)
return RedirectResponse(url="/contacts", status_code=303)
@router.post("/reorder")
async def reorder_contact(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("contacts", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/contacts"), status_code=303)

View File

@@ -216,3 +216,15 @@ async def delete_decision(decision_id: str, request: Request, db: AsyncSession =
repo = BaseRepository("decisions", db)
await repo.soft_delete(decision_id)
return RedirectResponse(url="/decisions", status_code=303)
@router.post("/reorder")
async def reorder_decision(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("decisions", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/decisions"), status_code=303)

View File

@@ -91,9 +91,11 @@ async def sync_files(db: AsyncSession):
))
db_records = [dict(r._mapping) for r in result]
# Build lookup sets
# Build lookup maps
all_db_paths = {r["storage_path"] for r in db_records}
active_db_paths = {r["storage_path"] for r in db_records if not r["is_deleted"]}
deleted_on_disk = {r["storage_path"]: r for r in db_records
if r["is_deleted"] and r["storage_path"] in disk_files}
# New on disk, not in DB at all → create record
new_files = disk_files - all_db_paths
@@ -116,6 +118,12 @@ async def sync_files(db: AsyncSession):
})
added += 1
# Soft-deleted in DB but still on disk → restore
for rel_path, record in deleted_on_disk.items():
repo = BaseRepository("files", db)
await repo.restore(record["id"])
added += 1
# Active in DB but missing from disk → soft-delete
missing_files = active_db_paths - disk_files
for record in db_records:
@@ -127,10 +135,36 @@ async def sync_files(db: AsyncSession):
return {"added": added, "removed": removed}
SORT_OPTIONS = {
"path": "storage_path ASC",
"path_desc": "storage_path DESC",
"name": "original_filename ASC",
"name_desc": "original_filename DESC",
"date": "created_at DESC",
"date_asc": "created_at ASC",
}
# Map type filter values to mime prefixes/patterns
TYPE_FILTERS = {
"image": "image/%",
"document": "application/pdf",
"text": "text/%",
"spreadsheet": "application/vnd.%sheet%",
"archive": "application/%zip%",
}
PER_PAGE = 50
@router.get("/")
async def list_files(
request: Request,
folder: Optional[str] = None,
sort: Optional[str] = None,
q: Optional[str] = None,
file_type: Optional[str] = None,
tag: Optional[str] = None,
page: int = 1,
context_type: Optional[str] = None,
context_id: Optional[str] = None,
db: AsyncSession = Depends(get_db),
@@ -142,38 +176,88 @@ async def list_files(
folders = get_folders()
order_by = SORT_OPTIONS.get(sort, "storage_path ASC")
# Normalize folder param from form: " " (space) = root, "" = all, None = all
if folder is not None:
if folder.strip() == "" and folder != "":
# Space-only value means root folder
folder = ""
elif folder.strip() == "":
# Empty string means "all folders" (no filter)
folder = None
# Build dynamic WHERE clauses
where_clauses = ["f.is_deleted = false"]
params = {}
if context_type and context_id:
# Files attached to a specific entity
result = await db.execute(text("""
where_clauses.append("fm.context_type = :ct AND fm.context_id = :cid")
params["ct"] = context_type
params["cid"] = context_id
if folder is not None:
if folder == "":
where_clauses.append("f.storage_path NOT LIKE '%/%'")
else:
where_clauses.append("f.storage_path LIKE :prefix")
params["prefix"] = folder + "/%"
if q and q.strip():
search_terms = q.strip().split()
tsquery = " & ".join(f"{t}:*" for t in search_terms)
where_clauses.append("f.search_vector @@ to_tsquery('english', :tsquery)")
params["tsquery"] = tsquery
if file_type and file_type in TYPE_FILTERS:
where_clauses.append("f.mime_type LIKE :mime_pattern")
params["mime_pattern"] = TYPE_FILTERS[file_type]
if tag and tag.strip():
where_clauses.append(":tag = ANY(f.tags)")
params["tag"] = tag.strip()
where_sql = " AND ".join(where_clauses)
# Count total for pagination
if context_type and context_id:
count_sql = f"""
SELECT COUNT(*) FROM files f
JOIN file_mappings fm ON fm.file_id = f.id
WHERE {where_sql}
"""
else:
count_sql = f"SELECT COUNT(*) FROM files f WHERE {where_sql}"
total = (await db.execute(text(count_sql), params)).scalar()
# Paginate
if page < 1:
page = 1
offset = (page - 1) * PER_PAGE
total_pages = max(1, (total + PER_PAGE - 1) // PER_PAGE)
# Query
if context_type and context_id:
query_sql = f"""
SELECT f.*, fm.context_type, fm.context_id
FROM files f
JOIN file_mappings fm ON fm.file_id = f.id
WHERE f.is_deleted = false AND fm.context_type = :ct AND fm.context_id = :cid
ORDER BY f.created_at DESC
"""), {"ct": context_type, "cid": context_id})
elif folder is not None:
if folder == "":
# Root folder: files with no directory separator in storage_path
result = await db.execute(text("""
SELECT * FROM files
WHERE is_deleted = false AND storage_path NOT LIKE '%/%'
ORDER BY created_at DESC
"""))
else:
# Specific folder: storage_path starts with folder/
result = await db.execute(text("""
SELECT * FROM files
WHERE is_deleted = false AND storage_path LIKE :prefix
ORDER BY created_at DESC
"""), {"prefix": folder + "/%"})
WHERE {where_sql}
ORDER BY {order_by}
LIMIT :lim OFFSET :off
"""
else:
# All files
result = await db.execute(text("""
SELECT * FROM files
WHERE is_deleted = false
ORDER BY created_at DESC
"""))
query_sql = f"""
SELECT f.* FROM files f
WHERE {where_sql}
ORDER BY {order_by}
LIMIT :lim OFFSET :off
"""
params["lim"] = PER_PAGE
params["off"] = offset
result = await db.execute(text(query_sql), params)
items = [dict(r._mapping) for r in result]
# Add derived folder field for display
@@ -181,9 +265,23 @@ async def list_files(
dirname = os.path.dirname(item["storage_path"])
item["folder"] = dirname if dirname else "/"
# Get all unique tags for the tag filter dropdown
tag_result = await db.execute(text(
"SELECT DISTINCT unnest(tags) AS tag FROM files WHERE is_deleted = false AND tags IS NOT NULL ORDER BY tag"
))
all_tags = [r._mapping["tag"] for r in tag_result]
return templates.TemplateResponse("files.html", {
"request": request, "sidebar": sidebar, "items": items,
"folders": folders, "current_folder": folder,
"current_sort": sort or "path",
"current_q": q or "",
"current_type": file_type or "",
"current_tag": tag or "",
"current_page": page,
"total_pages": total_pages,
"total_files": total,
"all_tags": all_tags,
"sync_result": sync_result,
"context_type": context_type or "",
"context_id": context_id or "",

View File

@@ -1,4 +1,4 @@
"""Daily Focus: date-scoped task commitment list."""
"""Daily Focus: date-scoped task/list-item commitment list."""
from fastapi import APIRouter, Request, Form, Depends
from fastapi.templating import Jinja2Templates
@@ -23,56 +23,133 @@ async def focus_view(
domain_id: Optional[str] = None,
area_id: Optional[str] = None,
project_id: Optional[str] = None,
search: Optional[str] = None,
source_type: Optional[str] = None,
db: AsyncSession = Depends(get_db),
):
sidebar = await get_sidebar_data(db)
target_date = date.fromisoformat(focus_date) if focus_date else date.today()
if not source_type:
source_type = "tasks"
# --- Focus items (both tasks and list items) ---
result = await db.execute(text("""
SELECT df.*, t.title, t.priority, t.status as task_status,
SELECT df.*,
t.title as title, t.priority, t.status as task_status,
t.project_id, t.due_date, t.estimated_minutes,
p.name as project_name,
d.name as domain_name, d.color as domain_color
COALESCE(p.name, lp.name) as project_name,
COALESCE(t.project_id, l.project_id) as effective_project_id,
COALESCE(d.name, ld.name) as domain_name,
COALESCE(d.color, ld.color) as domain_color,
COALESCE(d.id, ld.id) as effective_domain_id,
COALESCE(a.name, pa.name, la.name) as area_name,
COALESCE(a.id, pa.id, la.id) as effective_area_id,
li.content as list_item_content, li.list_id as list_item_list_id,
li.completed as list_item_completed,
l.name as list_name
FROM daily_focus df
JOIN tasks t ON df.task_id = t.id
LEFT JOIN tasks t ON df.task_id = t.id
LEFT JOIN projects p ON t.project_id = p.id
LEFT JOIN domains d ON t.domain_id = d.id
LEFT JOIN areas a ON t.area_id = a.id
LEFT JOIN areas pa ON p.area_id = pa.id
LEFT JOIN list_items li ON df.list_item_id = li.id
LEFT JOIN lists l ON li.list_id = l.id
LEFT JOIN projects lp ON l.project_id = lp.id
LEFT JOIN domains ld ON l.domain_id = ld.id
LEFT JOIN areas la ON l.area_id = la.id
WHERE df.focus_date = :target_date AND df.is_deleted = false
AND (t.id IS NULL OR t.is_deleted = false)
AND (li.id IS NULL OR li.is_deleted = false)
ORDER BY df.sort_order, df.created_at
"""), {"target_date": target_date})
items = [dict(r._mapping) for r in result]
# Available tasks to add (open, not already in today's focus)
avail_where = [
"t.is_deleted = false",
"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)",
]
avail_params = {"target_date": target_date}
# Group items by domain only — area/project shown as inline columns
from collections import OrderedDict
domain_map = OrderedDict()
for item in items:
dk = item.get("effective_domain_id") or "__none__"
dl = item.get("domain_name") or "General"
dc = item.get("domain_color") or ""
if dk not in domain_map:
domain_map[dk] = {"key": dk, "label": dl, "color": dc, "rows": []}
domain_map[dk]["rows"].append(item)
if domain_id:
avail_where.append("t.domain_id = :domain_id")
avail_params["domain_id"] = domain_id
if area_id:
avail_where.append("t.area_id = :area_id")
avail_params["area_id"] = area_id
if project_id:
avail_where.append("t.project_id = :project_id")
avail_params["project_id"] = project_id
hierarchy = list(domain_map.values())
avail_sql = " AND ".join(avail_where)
# --- Available tasks ---
available_tasks = []
if source_type == "tasks":
avail_where = [
"t.is_deleted = false",
"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)",
]
avail_params = {"target_date": target_date}
result = await db.execute(text(f"""
SELECT t.id, t.title, t.priority, t.due_date,
p.name as project_name, d.name as domain_name
FROM tasks t
LEFT JOIN projects p ON t.project_id = p.id
LEFT JOIN domains d ON t.domain_id = d.id
WHERE {avail_sql}
ORDER BY t.priority ASC, t.due_date ASC NULLS LAST
LIMIT 50
"""), avail_params)
available_tasks = [dict(r._mapping) for r in result]
if search:
avail_where.append("t.title ILIKE :search")
avail_params["search"] = f"%{search}%"
if domain_id:
avail_where.append("t.domain_id = :domain_id")
avail_params["domain_id"] = domain_id
if area_id:
avail_where.append("t.area_id = :area_id")
avail_params["area_id"] = area_id
if project_id:
avail_where.append("t.project_id = :project_id")
avail_params["project_id"] = project_id
avail_sql = " AND ".join(avail_where)
result = await db.execute(text(f"""
SELECT t.id, t.title, t.priority, t.due_date,
p.name as project_name, d.name as domain_name
FROM tasks t
LEFT JOIN projects p ON t.project_id = p.id
LEFT JOIN domains d ON t.domain_id = d.id
WHERE {avail_sql}
ORDER BY t.priority ASC, t.due_date ASC NULLS LAST
LIMIT 200
"""), avail_params)
available_tasks = [dict(r._mapping) for r in result]
# --- Available list items ---
available_list_items = []
if source_type == "list_items":
li_where = [
"li.is_deleted = false",
"li.completed = 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_params = {"target_date": target_date}
if search:
li_where.append("li.content ILIKE :search")
li_params["search"] = f"%{search}%"
if domain_id:
li_where.append("l.domain_id = :domain_id")
li_params["domain_id"] = domain_id
if area_id:
li_where.append("l.area_id = :area_id")
li_params["area_id"] = area_id
if project_id:
li_where.append("l.project_id = :project_id")
li_params["project_id"] = project_id
li_sql = " AND ".join(li_where)
result = await db.execute(text(f"""
SELECT li.id, li.content, li.list_id, l.name as list_name,
d.name as domain_name
FROM list_items li
JOIN lists l ON li.list_id = l.id
LEFT JOIN domains d ON l.domain_id = d.id
WHERE {li_sql}
ORDER BY l.name ASC, li.sort_order ASC
LIMIT 200
"""), li_params)
available_list_items = [dict(r._mapping) for r in result]
# Estimated total minutes
total_est = sum(i.get("estimated_minutes") or 0 for i in items)
@@ -87,13 +164,17 @@ async def focus_view(
return templates.TemplateResponse("focus.html", {
"request": request, "sidebar": sidebar,
"items": items, "available_tasks": available_tasks,
"items": items, "hierarchy": hierarchy,
"available_tasks": available_tasks,
"available_list_items": available_list_items,
"focus_date": target_date,
"total_estimated": total_est,
"domains": domains, "areas": areas, "projects": projects,
"current_domain_id": domain_id or "",
"current_area_id": area_id or "",
"current_project_id": project_id or "",
"current_search": search or "",
"current_source_type": source_type,
"page_title": "Daily Focus", "active_nav": "focus",
})
@@ -101,8 +182,9 @@ async def focus_view(
@router.post("/add")
async def add_to_focus(
request: Request,
task_id: str = Form(...),
focus_date: str = Form(...),
task_id: Optional[str] = Form(None),
list_item_id: Optional[str] = Form(None),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("daily_focus", db)
@@ -114,10 +196,45 @@ async def add_to_focus(
"""), {"fd": parsed_date})
next_order = result.scalar()
await repo.create({
"task_id": task_id, "focus_date": parsed_date,
"sort_order": next_order, "completed": False,
})
data = {
"focus_date": parsed_date,
"sort_order": next_order,
"completed": False,
}
if task_id:
data["task_id"] = task_id
if list_item_id:
data["list_item_id"] = list_item_id
await repo.create(data)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@router.post("/reorder")
async def reorder_focus(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
focus_date: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("daily_focus", db)
parsed_date = date.fromisoformat(focus_date)
await repo.move_in_order(item_id, direction, filters={"focus_date": parsed_date})
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@router.post("/reorder-all")
async def reorder_all_focus(
request: Request,
item_ids: str = Form(...),
focus_date: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("daily_focus", db)
ids = [i.strip() for i in item_ids.split(",") if i.strip()]
if ids:
await repo.reorder(ids)
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)
@@ -127,12 +244,25 @@ async def toggle_focus(focus_id: str, request: Request, db: AsyncSession = Depen
item = await repo.get(focus_id)
if item:
await repo.update(focus_id, {"completed": not item["completed"]})
# Also toggle the task status
task_repo = BaseRepository("tasks", db)
if not item["completed"]:
await task_repo.update(item["task_id"], {"status": "done", "completed_at": datetime.now(timezone.utc)})
else:
await task_repo.update(item["task_id"], {"status": "open", "completed_at": None})
if item["task_id"]:
# Sync task status
task_repo = BaseRepository("tasks", db)
if not item["completed"]:
await task_repo.update(item["task_id"], {"status": "done", "completed_at": datetime.now(timezone.utc)})
else:
await task_repo.update(item["task_id"], {"status": "open", "completed_at": None})
elif item["list_item_id"]:
# Sync list item completed status
now = datetime.now(timezone.utc)
if not item["completed"]:
await db.execute(text(
"UPDATE list_items SET completed = true, completed_at = :now, updated_at = :now WHERE id = :id"
), {"id": item["list_item_id"], "now": now})
else:
await db.execute(text(
"UPDATE list_items SET completed = false, completed_at = NULL, updated_at = :now WHERE id = :id"
), {"id": item["list_item_id"], "now": now})
await db.commit()
focus_date = item["focus_date"] if item else date.today()
return RedirectResponse(url=f"/focus?focus_date={focus_date}", status_code=303)

View File

@@ -154,3 +154,15 @@ async def delete_link(link_id: str, request: Request, db: AsyncSession = Depends
repo = BaseRepository("links", db)
await repo.soft_delete(link_id)
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)
@router.post("/reorder")
async def reorder_link(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("links", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/links"), status_code=303)

View File

@@ -332,3 +332,35 @@ async def remove_contact(
"DELETE FROM contact_lists WHERE contact_id = :cid AND list_id = :lid"
), {"cid": contact_id, "lid": list_id})
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)
@router.post("/reorder")
async def reorder_list(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("lists", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/lists"), status_code=303)
@router.post("/{list_id}/items/reorder")
async def reorder_list_item(
list_id: str,
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
parent_id: Optional[str] = Form(None),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("list_items", db)
filters = {"list_id": list_id}
if parent_id:
filters["parent_item_id"] = parent_id
else:
# Top-level items only (no parent)
filters["parent_item_id"] = None
await repo.move_in_order(item_id, direction, filters=filters)
return RedirectResponse(url=f"/lists/{list_id}", status_code=303)

View File

@@ -404,3 +404,15 @@ async def remove_contact(
"DELETE FROM contact_meetings WHERE contact_id = :cid AND meeting_id = :mid"
), {"cid": contact_id, "mid": meeting_id})
return RedirectResponse(url=f"/meetings/{meeting_id}?tab=contacts", status_code=303)
@router.post("/reorder")
async def reorder_meeting(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("meetings", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/meetings"), status_code=303)

View File

@@ -193,3 +193,15 @@ async def delete_note(note_id: str, request: Request, db: AsyncSession = Depends
await repo.soft_delete(note_id)
referer = request.headers.get("referer", "/notes")
return RedirectResponse(url=referer, status_code=303)
@router.post("/reorder")
async def reorder_note(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("notes", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/notes"), status_code=303)

View File

@@ -33,6 +33,7 @@ async def list_tasks(
status: Optional[str] = None,
priority: Optional[str] = None,
context: Optional[str] = None,
search: Optional[str] = None,
sort: str = "sort_order",
db: AsyncSession = Depends(get_db),
):
@@ -40,6 +41,9 @@ async def list_tasks(
where_clauses = ["t.is_deleted = false"]
params = {}
if search:
where_clauses.append("t.title ILIKE :search")
params["search"] = f"%{search}%"
if domain_id:
where_clauses.append("t.domain_id = :domain_id")
params["domain_id"] = domain_id
@@ -97,6 +101,7 @@ async def list_tasks(
return templates.TemplateResponse("tasks.html", {
"request": request, "sidebar": sidebar, "items": items,
"domains": domains, "projects": projects, "context_types": context_types,
"current_search": search or "",
"current_domain_id": domain_id or "",
"current_project_id": project_id or "",
"current_status": status or "",
@@ -422,8 +427,7 @@ async def toggle_task(task_id: str, request: Request, db: AsyncSession = Depends
async def delete_task(task_id: str, request: Request, db: AsyncSession = Depends(get_db)):
repo = BaseRepository("tasks", db)
await repo.soft_delete(task_id)
referer = request.headers.get("referer", "/tasks")
return RedirectResponse(url=referer, status_code=303)
return RedirectResponse(url="/tasks", status_code=303)
# Quick add from any task list
@@ -481,3 +485,15 @@ async def remove_contact(
"DELETE FROM contact_tasks WHERE contact_id = :cid AND task_id = :tid"
), {"cid": contact_id, "tid": task_id})
return RedirectResponse(url=f"/tasks/{task_id}?tab=contacts", status_code=303)
@router.post("/reorder")
async def reorder_task(
request: Request,
item_id: str = Form(...),
direction: str = Form(...),
db: AsyncSession = Depends(get_db),
):
repo = BaseRepository("tasks", db)
await repo.move_in_order(item_id, direction)
return RedirectResponse(url=request.headers.get("referer", "/tasks"), status_code=303)

View File

@@ -152,11 +152,11 @@ a:hover { color: var(--accent-hover); }
.nav-item {
display: flex;
align-items: center;
font-size: 0.80rem;
gap: 8px;
padding: 7px 10px;
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 0.92rem;
font-weight: 500;
cursor: pointer;
transition: all var(--transition);
@@ -201,7 +201,7 @@ a:hover { color: var(--accent-hover); }
align-items: center;
gap: 6px;
padding: 6px 10px;
font-size: 0.85rem;
font-size: 0.80rem;
font-weight: 600;
color: var(--text);
cursor: pointer;
@@ -233,7 +233,7 @@ a:hover { color: var(--accent-hover); }
.project-link {
display: block;
padding: 4px 10px 4px 18px;
font-size: 0.85rem;
font-size: 0.80rem;
color: var(--text-secondary);
border-radius: var(--radius-sm);
white-space: nowrap;
@@ -401,8 +401,8 @@ a:hover { color: var(--accent-hover); }
.list-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
gap: 8px;
padding: 6px 12px;
border-bottom: 1px solid var(--border);
transition: background var(--transition);
}
@@ -466,6 +466,7 @@ a:hover { color: var(--accent-hover); }
.row-title {
flex: 1;
font-size: 0.80rem;
font-weight: 500;
min-width: 0;
overflow: hidden;
@@ -735,6 +736,9 @@ a:hover { color: var(--accent-hover); }
.detail-body {
line-height: 1.75;
color: var(--text);
overflow-wrap: break-word;
word-break: break-word;
min-width: 0;
}
.detail-body p { margin-bottom: 1em; }
@@ -792,17 +796,15 @@ a:hover { color: var(--accent-hover); }
.capture-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 8px;
gap: 8px;
padding: 6px 12px;
border-bottom: 1px solid var(--border);
transition: background var(--transition);
}
.capture-text {
flex: 1;
font-size: 0.92rem;
font-size: 0.80rem;
}
.capture-actions {
@@ -815,20 +817,17 @@ a:hover { color: var(--accent-hover); }
.focus-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 6px;
transition: all var(--transition);
gap: 8px;
padding: 6px 12px;
border-bottom: 1px solid var(--border);
transition: background var(--transition);
}
.focus-item:hover { border-color: var(--accent); }
.focus-item:hover { background: var(--surface2); }
.focus-item.completed { opacity: 0.6; }
.focus-item.completed .focus-title { text-decoration: line-through; }
.focus-title { flex: 1; font-weight: 500; }
.focus-title { flex: 1; font-size: 0.80rem; font-weight: 500; }
.focus-meta { font-size: 0.78rem; color: var(--muted); }
/* ---- Alerts ---- */
@@ -1108,6 +1107,43 @@ a:hover { color: var(--accent-hover); }
transition: width 0.3s;
}
/* ---- Focus Domain Toggle ---- */
.focus-domain-header:hover td { background: var(--surface3); }
/* ---- Focus Drag-and-Drop ---- */
.focus-drag-row { cursor: grab; }
.focus-drag-row.dragging { opacity: 0.4; background: var(--accent-soft); }
.focus-drag-row.drag-over { box-shadow: 0 -2px 0 0 var(--accent) inset; }
/* ---- Reorder Grip Handle ---- */
.reorder-grip {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: 0;
margin-right: 4px;
opacity: 0.3;
transition: opacity 0.15s;
flex-shrink: 0;
}
.reorder-grip:hover { opacity: 0.9; }
.reorder-grip form { display: block; margin: 0; padding: 0; line-height: 0; }
.grip-btn {
display: block;
background: none;
border: none;
color: var(--muted);
cursor: pointer;
font-size: 10px;
width: 12px;
height: 10px;
padding: 0;
line-height: 10px;
transition: color 0.15s;
text-align: center;
}
.grip-btn:hover { color: var(--accent); }
/* ---- Scrollbar ---- */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }

View File

@@ -22,7 +22,7 @@
{% set appt_date = appt.start_at.strftime('%A, %B %-d, %Y') if appt.start_at else 'No Date' %}
{% if appt_date != current_date.value %}
{% if not loop.first %}</div>{% endif %}
<div class="date-group-label" style="padding: 12px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
<div class="date-group-label" style="padding: 6px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
{{ appt_date }}
</div>
<div>
@@ -30,6 +30,9 @@
{% endif %}
<div class="list-row">
{% with reorder_url="/appointments/reorder", item_id=appt.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<div style="flex-shrink: 0; min-width: 60px;">
{% if appt.all_day %}
<span class="status-badge status-active" style="font-size: 0.72rem;">All Day</span>

View File

@@ -53,11 +53,12 @@
</div>
{% if items %}
<div class="card">
{% for item in items %}
{# Batch header #}
{% if item._batch_first and item.import_batch_id %}
<div class="flex items-center justify-between mb-2 mt-3" style="padding:6px 12px;background:var(--surface2);border-radius:var(--radius-sm)">
<div class="flex items-center justify-between" style="padding:6px 12px;background:var(--surface2);">
<span class="text-xs text-muted">Batch &middot; {{ batches[item.import_batch_id|string] }} items</span>
<form action="/capture/batch/{{ item.import_batch_id }}/undo" method="post" style="display:inline" data-confirm="Delete all items in this batch?">
<button class="btn btn-ghost btn-xs" style="color:var(--red)">Undo batch</button>
@@ -99,6 +100,7 @@
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-icon">&#128229;</div>

View File

@@ -8,6 +8,9 @@
<div class="card">
{% for item in items %}
<div class="list-row">
{% with reorder_url="/contacts/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/contacts/{{ item.id }}">{{ item.first_name }} {{ item.last_name or '' }}</a></span>
{% if item.company %}<span class="row-tag">{{ item.company }}</span>{% endif %}
{% if item.role %}<span class="row-meta">{{ item.role }}</span>{% endif %}

View File

@@ -25,6 +25,9 @@
<div class="card mt-3">
{% for item in items %}
<div class="list-row">
{% with reorder_url="/decisions/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/decisions/{{ item.id }}">{{ item.title }}</a></span>
<span class="status-badge status-{{ item.status }}">{{ item.status }}</span>
<span class="row-tag impact-{{ item.impact }}">{{ item.impact }}</span>

View File

@@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Files<span class="page-count">{{ items|length }}</span></h1>
<h1 class="page-title">Files<span class="page-count">{{ total_files }}</span></h1>
<div class="flex gap-2">
<form action="/files/sync" method="post" style="display:inline">
<button type="submit" class="btn btn-secondary">Sync Files</button>
@@ -16,47 +16,140 @@
</div>
{% endif %}
{% if folders %}
<div class="filter-bar" style="display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px;">
<a href="/files" class="btn btn-xs {{ 'btn-primary' if current_folder is none else 'btn-ghost' }}">All</a>
<a href="/files?folder=" class="btn btn-xs {{ 'btn-primary' if current_folder is not none and current_folder == '' else 'btn-ghost' }}">/</a>
{% for f in folders %}
<a href="/files?folder={{ f }}" class="btn btn-xs {{ 'btn-primary' if current_folder == f else 'btn-ghost' }}">{{ f }}</a>
<form class="filters-bar" method="get" action="/files" style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 16px;">
<input type="text" name="q" value="{{ current_q }}" class="form-input" placeholder="Search files..." style="max-width: 220px; padding: 6px 10px; font-size: 0.85rem;">
<select name="folder" class="form-input" style="max-width: 200px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
<option value="" {{ 'selected' if current_folder is none }}>All folders</option>
<option value=" " {{ 'selected' if current_folder is not none and current_folder == '' }}>/ (root)</option>
{% for f in folders %}
<option value="{{ f }}" {{ 'selected' if current_folder == f }}>{% if '/' in f %}&nbsp;&nbsp;{{ f.split('/')[-1] }}{% else %}{{ f }}{% endif %}</option>
{% endfor %}
</select>
<select name="file_type" class="form-input" style="max-width: 160px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
<option value="">All types</option>
<option value="image" {{ 'selected' if current_type == 'image' }}>Images</option>
<option value="document" {{ 'selected' if current_type == 'document' }}>Documents</option>
<option value="text" {{ 'selected' if current_type == 'text' }}>Text</option>
<option value="spreadsheet" {{ 'selected' if current_type == 'spreadsheet' }}>Spreadsheets</option>
<option value="archive" {{ 'selected' if current_type == 'archive' }}>Archives</option>
</select>
{% if all_tags %}
<select name="tag" class="form-input" style="max-width: 160px; padding: 6px 10px; font-size: 0.85rem;" onchange="this.form.submit()">
<option value="">All tags</option>
{% for t in all_tags %}
<option value="{{ t }}" {{ 'selected' if current_tag == t }}>{{ t }}</option>
{% endfor %}
</select>
{% endif %}
{% if current_q or current_type or current_tag or current_folder is not none %}
<input type="hidden" name="sort" value="{{ current_sort }}">
<button type="submit" class="btn btn-primary btn-xs">Search</button>
<a href="/files" class="btn btn-ghost btn-xs">Clear</a>
{% else %}
<button type="submit" class="btn btn-primary btn-xs">Search</button>
{% endif %}
</form>
{% set qp = [] %}
{% if current_folder is not none %}{% if current_folder == '' %}{{ qp.append('folder= ') or '' }}{% else %}{{ qp.append('folder=' ~ current_folder) or '' }}{% endif %}{% endif %}
{% if current_q %}{{ qp.append('q=' ~ current_q) or '' }}{% endif %}
{% if current_type %}{{ qp.append('file_type=' ~ current_type) or '' }}{% endif %}
{% if current_tag %}{{ qp.append('tag=' ~ current_tag) or '' }}{% endif %}
{% set filter_qs = qp | join('&') %}
{% set sort_base = '/files?' ~ (filter_qs ~ '&' if filter_qs else '') %}
{% if items %}
<div class="card" style="overflow-x: auto;">
<table class="data-table" style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 1px solid var(--border); text-align: left;">
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
<a href="{{ sort_base }}sort={{ 'path_desc' if current_sort == 'path' else 'path' }}" style="color: var(--muted); text-decoration: none;">
Path {{ '▲' if current_sort == 'path' else ('▼' if current_sort == 'path_desc' else '') }}
</a>
</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
<a href="{{ sort_base }}sort={{ 'name_desc' if current_sort == 'name' else 'name' }}" style="color: var(--muted); text-decoration: none;">
Name {{ '▲' if current_sort == 'name' else ('▼' if current_sort == 'name_desc' else '') }}
</a>
</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Type</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">Size</th>
<th style="padding: 6px 12px; font-weight: 600; font-size: 0.8rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em;">
<a href="{{ sort_base }}sort={{ 'date_asc' if current_sort == 'date' else 'date' }}" style="color: var(--muted); text-decoration: none;">
Date {{ '▼' if current_sort == 'date' else ('▲' if current_sort == 'date_asc' else '') }}
</a>
</th>
<th style="padding: 6px 12px;"></th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr style="border-bottom: 1px solid var(--border);">
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem;">{{ item.folder }}</td>
<td style="padding: 6px 12px; font-size: 0.80rem;">
<a href="/files/{{ item.id }}/preview" style="color: var(--accent);">{{ item.original_filename }}</a>
</td>
<td style="padding: 6px 12px;">
{% if item.mime_type %}<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>{% endif %}
</td>
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem; white-space: nowrap;">
{% if item.size_bytes %}{{ "%.1f"|format(item.size_bytes / 1024) }} KB{% endif %}
</td>
<td style="padding: 6px 12px; color: var(--muted); font-size: 0.80rem; white-space: nowrap;">
{{ item.created_at.strftime('%Y-%m-%d') if item.created_at else '' }}
</td>
<td style="padding: 6px 12px; text-align: right; white-space: nowrap;">
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if total_pages > 1 %}
{% set page_base = sort_base ~ 'sort=' ~ current_sort ~ '&' %}
<div style="display: flex; justify-content: center; align-items: center; gap: 8px; margin-top: 16px;">
{% if current_page > 1 %}
<a href="{{ page_base }}page={{ current_page - 1 }}" class="btn btn-ghost btn-xs">Prev</a>
{% endif %}
{% for p in range(1, total_pages + 1) %}
{% if p == current_page %}
<span class="btn btn-primary btn-xs" style="pointer-events: none;">{{ p }}</span>
{% elif p <= 3 or p >= total_pages - 2 or (p >= current_page - 1 and p <= current_page + 1) %}
<a href="{{ page_base }}page={{ p }}" class="btn btn-ghost btn-xs">{{ p }}</a>
{% elif p == 4 and current_page > 5 %}
<span style="color: var(--muted);">...</span>
{% elif p == total_pages - 3 and current_page < total_pages - 4 %}
<span style="color: var(--muted);">...</span>
{% endif %}
{% endfor %}
{% if current_page < total_pages %}
<a href="{{ page_base }}page={{ current_page + 1 }}" class="btn btn-ghost btn-xs">Next</a>
{% endif %}
</div>
{% endif %}
{% if items %}
<div class="card">
{% for item in items %}
<div class="list-row">
<span class="row-title">
<a href="/files/{{ item.id }}/preview">{{ item.original_filename }}</a>
</span>
<span class="row-meta" style="color: var(--muted); font-size: 0.8rem;">{{ item.folder }}</span>
{% if item.mime_type %}
<span class="row-tag">{{ item.mime_type.split('/')|last }}</span>
{% endif %}
{% if item.size_bytes %}
<span class="row-meta">{{ "%.1f"|format(item.size_bytes / 1024) }} KB</span>
{% endif %}
{% if item.description %}
<span class="row-meta">{{ item.description[:50] }}</span>
{% endif %}
<div class="row-actions">
<a href="/files/{{ item.id }}/download" class="btn btn-ghost btn-xs">Download</a>
<form action="/files/{{ item.id }}/delete" method="post" data-confirm="Delete this file?" style="display:inline">
<button type="submit" class="btn btn-ghost btn-xs" style="color: var(--red)">Del</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-icon">&#128193;</div>
{% if current_q or current_type or current_tag %}
<div class="empty-state-text">No files match your filters</div>
<a href="/files" class="btn btn-secondary">Clear Filters</a>
{% else %}
<div class="empty-state-text">No files{{ ' in this folder' if current_folder is not none else ' uploaded yet' }}</div>
<a href="/files/upload" class="btn btn-primary">Upload First File</a>
{% endif %}
</div>
{% endif %}
{% endblock %}

View File

@@ -12,71 +12,150 @@
<span class="text-sm" style="font-weight:600">{{ focus_date }}</span>
</div>
<!-- Focus items -->
<!-- Focus items grouped by domain -->
{% if items %}
{% for item in items %}
<div class="focus-item {{ 'completed' if item.completed }}">
<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>
</form>
<span class="priority-dot priority-{{ item.priority }}"></span>
<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}
{% if item.estimated_minutes %}<span class="focus-meta">~{{ item.estimated_minutes }}min</span>{% endif %}
{% if item.due_date %}<span class="focus-meta">{{ item.due_date }}</span>{% endif %}
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">&times;</button>
</form>
</div>
<div class="card">
<table id="focus-table" style="width:100%;border-collapse:collapse;font-size:0.8rem;">
<colgroup>
<col style="width:18px"><col style="width:24px"><col style="width:20px"><col style="width:74px"><col style="width:10px">
<col><col style="width:110px"><col style="width:120px"><col style="width:50px"><col style="width:24px">
</colgroup>
{% for domain in hierarchy %}
<tbody>
<tr class="focus-domain-header" data-domain-key="{{ domain.key }}" style="cursor:pointer;">
<td colspan="10" style="padding:2px 8px;border-bottom:1px solid var(--border);background:var(--surface2);">
<span style="display:inline-flex;align-items:center;gap:6px;">
<svg class="focus-domain-chevron" width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="2" style="transition:transform 0.15s;flex-shrink:0;color:var(--muted);"><polyline points="4 2 8 6 4 10"/></svg>
<span style="width:8px;height:8px;border-radius:50%;background:{{ domain.color or 'var(--accent)' }};flex-shrink:0;"></span>
<span style="font-weight:700;font-size:0.8rem;letter-spacing:0.03em;text-transform:uppercase;color:var(--text)">{{ domain.label }}</span>
<span style="font-size:0.72rem;color:var(--muted);font-weight:400;">{{ domain.rows|length }}</span>
</span>
</td>
</tr>
</tbody>
<tbody class="focus-drag-group" data-domain-key="{{ domain.key }}">
{% 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 }}">
<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;">
<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>
</form>
</td>
<td style="padding:1px 3px;vertical-align:middle;color:var(--muted);font-size:0.78rem;white-space:nowrap;">{{ item.due_date or '' }}</td>
<td style="padding:1px 1px;vertical-align:middle;">{% if item.task_id %}<span class="priority-dot priority-{{ item.priority }}"></span>{% elif item.list_item_id %}<span style="color:var(--muted);font-size:0.85rem;">&#9776;</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;{{ 'text-decoration:line-through;' if item.completed }}">{% if item.task_id %}{% if item.title %}<a href="/tasks/{{ item.task_id }}" class="focus-title">{{ item.title }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% elif item.list_item_id %}{% if item.list_item_content %}<a href="/lists/{{ item.list_item_list_id }}" class="focus-title">{{ item.list_item_content }}</a>{% else %}<span style="color:var(--muted)">[Deleted]</span>{% endif %}{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.area_name %}<span class="row-tag">{{ item.area_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{% if item.task_id and item.project_name %}<span class="row-tag" style="background:var(--accent-soft);color:var(--accent)">{{ item.project_name }}</span>{% elif item.list_item_id and item.list_name %}<span class="row-tag" style="background:var(--purple);color:#fff">{{ item.list_name }}</span>{% endif %}</td>
<td style="padding:1px 3px;vertical-align:middle;text-align:right;color:var(--muted);font-size:0.78rem;">{{ '~%smin'|format(item.estimated_minutes) if item.estimated_minutes else '' }}</td>
<td style="padding:1px 1px;vertical-align:middle;">
<form action="/focus/{{ item.id }}/remove" method="post" style="display:inline">
<button class="btn btn-ghost btn-xs" style="color:var(--red)" title="Remove from focus">&times;</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
<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="focus_date" value="{{ focus_date }}">
</form>
</div>
{% else %}
<div class="empty-state mb-4"><div class="empty-state-text">No focus items for this day</div></div>
{% endif %}
<!-- Add task to focus -->
<div class="card mt-4">
<div class="card-header"><h2 class="card-title">Add to Focus</h2></div>
<!-- Add to Focus -->
<div class="card mt-3">
<div class="card-header" style="padding:4px 10px;"><h2 class="card-title" style="font-size:0.85rem;margin:0;">Add to Focus</h2></div>
<form class="filters-bar" method="get" action="/focus" id="focus-filters" style="padding: 8px 12px; border-bottom: 1px solid var(--border);">
<!-- Tab strip -->
<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 %}"
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 %}"
class="tab-item {{ 'active' if current_source_type == 'list_items' }}">List Items</a>
</div>
<!-- Filters -->
<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="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()">
<option value="">All Domains</option>
{% for d in domains %}
<option value="{{ d.id }}" {{ 'selected' if current_domain_id == d.id|string }}>{{ d.name }}</option>
{% endfor %}
</select>
{% if current_source_type == 'tasks' %}
<select name="area_id" class="filter-select" onchange="this.form.submit()">
<option value="">All Areas</option>
{% for a in areas %}
<option value="{{ a.id }}" {{ 'selected' if current_area_id == a.id|string }}>{{ a.name }}</option>
{% endfor %}
</select>
{% endif %}
<select name="project_id" class="filter-select" id="focus-project" onchange="this.form.submit()">
<option value="">All Projects</option>
{% for p in projects %}
<option value="{{ p.id }}" {{ 'selected' if current_project_id == p.id|string }}>{{ p.name }}</option>
{% endfor %}
</select>
<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 %}
<a href="/focus?focus_date={{ focus_date }}&source_type={{ current_source_type }}" class="btn btn-ghost btn-xs" style="color:var(--red)">Clear</a>
{% endif %}
</form>
{% for t in available_tasks[:15] %}
<div class="list-row">
<span class="priority-dot priority-{{ t.priority }}"></span>
<span class="row-title">{{ t.title }}</span>
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
{% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %}
<form action="/focus/add" method="post" style="display:inline">
<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>
</form>
</div>
{% else %}
<div style="padding: 16px; color: var(--muted); font-size: 0.85rem;">No available tasks matching filters</div>
{% endfor %}
<!-- Available tasks -->
{% if current_source_type == 'tasks' %}
{% for t in available_tasks %}
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}" style="padding:3px 8px;min-height:0;">
<span class="priority-dot priority-{{ t.priority }}"></span>
<span class="row-title">{{ t.title }}</span>
{% if t.project_name %}<span class="row-tag">{{ t.project_name }}</span>{% endif %}
{% if t.due_date %}<span class="row-meta">{{ t.due_date }}</span>{% endif %}
<form action="/focus/add" method="post" style="display:inline">
<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>
</form>
</div>
{% else %}
<div style="padding: 8px 10px; color: var(--muted); font-size: 0.8rem;">No available tasks matching filters</div>
{% endfor %}
{% if available_tasks|length > 25 %}
<div style="padding: 4px 10px; text-align:center;" id="focus-show-more-wrap">
<button class="btn btn-ghost btn-xs" id="focus-show-more">Show all {{ available_tasks|length }} tasks</button>
</div>
{% endif %}
<!-- Available list items -->
{% elif current_source_type == 'list_items' %}
{% for li in available_list_items %}
<div class="list-row{% if loop.index > 25 %} focus-hidden-item hidden{% endif %}" style="padding:3px 8px;min-height:0;">
<span style="color:var(--muted);font-size:0.85rem;margin-right:4px">&#9776;</span>
<span class="row-title">{{ li.content }}</span>
{% 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">
<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>
</form>
</div>
{% else %}
<div style="padding: 8px 10px; color: var(--muted); font-size: 0.8rem;">No available list items matching filters</div>
{% endfor %}
{% if available_list_items|length > 25 %}
<div style="padding: 4px 10px; text-align:center;" id="focus-show-more-wrap">
<button class="btn btn-ghost btn-xs" id="focus-show-more">Show all {{ available_list_items|length }} items</button>
</div>
{% endif %}
{% endif %}
</div>
<script>
@@ -86,23 +165,132 @@
var currentProjectId = '{{ current_project_id }}';
var form = document.getElementById('focus-filters');
domainSel.addEventListener('change', function() {
var did = domainSel.value;
if (!did) { form.submit(); return; }
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
.then(function(r) { return r.json(); })
.then(function(projects) {
projectSel.innerHTML = '<option value="">All Projects</option>';
projects.forEach(function(p) {
var opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
if (p.id === currentProjectId) opt.selected = true;
projectSel.appendChild(opt);
if (domainSel) {
domainSel.addEventListener('change', function() {
var did = domainSel.value;
if (!did || !projectSel) { form.submit(); return; }
fetch('/projects/api/by-domain?domain_id=' + encodeURIComponent(did))
.then(function(r) { return r.json(); })
.then(function(projects) {
projectSel.innerHTML = '<option value="">All Projects</option>';
projects.forEach(function(p) {
var opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
if (p.id === currentProjectId) opt.selected = true;
projectSel.appendChild(opt);
});
form.submit();
})
.catch(function() { form.submit(); });
});
}
// Show more button
var showMoreBtn = document.getElementById('focus-show-more');
if (showMoreBtn) {
showMoreBtn.addEventListener('click', function() {
var hidden = document.querySelectorAll('.focus-hidden-item.hidden');
hidden.forEach(function(el) { el.classList.remove('hidden'); });
document.getElementById('focus-show-more-wrap').style.display = 'none';
});
}
// Domain group collapse/expand
document.querySelectorAll('.focus-domain-header').forEach(function(header) {
var key = 'focus-domain-' + header.dataset.domainKey;
var tbody = document.querySelector('tbody.focus-drag-group[data-domain-key="' + header.dataset.domainKey + '"]');
var chevron = header.querySelector('.focus-domain-chevron');
// Restore saved state
if (localStorage.getItem(key) === 'true') {
tbody.style.display = 'none';
chevron.style.transform = 'rotate(0deg)';
} else {
chevron.style.transform = 'rotate(90deg)';
}
header.addEventListener('click', function() {
var hidden = tbody.style.display === 'none';
tbody.style.display = hidden ? '' : 'none';
chevron.style.transform = hidden ? 'rotate(90deg)' : 'rotate(0deg)';
localStorage.setItem(key, !hidden);
});
});
// Renumber rows within each domain group
function renumberFocusRows() {
document.querySelectorAll('.focus-drag-group').forEach(function(tbody) {
var rows = tbody.querySelectorAll('.focus-drag-row');
rows.forEach(function(row, i) {
var numCell = row.querySelector('.focus-row-num');
if (numCell) numCell.textContent = i + 1;
});
});
}
// Drag-and-drop reorder within domain groups
var dragRow = null;
var dragGroup = null;
document.querySelectorAll('.focus-drag-row').forEach(function(row) {
row.addEventListener('dragstart', function(e) {
dragRow = row;
dragGroup = row.closest('.focus-drag-group');
row.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', row.dataset.id);
});
row.addEventListener('dragend', function() {
row.classList.remove('dragging');
document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) {
el.classList.remove('drag-over');
});
// Collect all IDs across all groups in DOM order and submit
if (dragRow) {
var allIds = [];
document.querySelectorAll('#focus-table .focus-drag-row').forEach(function(r) {
allIds.push(r.dataset.id);
});
form.submit();
})
.catch(function() { form.submit(); });
document.getElementById('focus-reorder-ids').value = allIds.join(',');
document.getElementById('focus-reorder-form').submit();
}
dragRow = null;
dragGroup = null;
});
row.addEventListener('dragover', function(e) {
e.preventDefault();
if (!dragRow || row === dragRow) return;
// Constrain to same domain group
if (row.closest('.focus-drag-group') !== dragGroup) return;
e.dataTransfer.dropEffect = 'move';
document.querySelectorAll('.focus-drag-row.drag-over').forEach(function(el) {
el.classList.remove('drag-over');
});
row.classList.add('drag-over');
});
row.addEventListener('dragleave', function() {
row.classList.remove('drag-over');
});
row.addEventListener('drop', function(e) {
e.preventDefault();
if (!dragRow || row === dragRow) return;
if (row.closest('.focus-drag-group') !== dragGroup) return;
row.classList.remove('drag-over');
// Insert dragged row before or after target based on position
var tbody = dragGroup;
var rows = Array.from(tbody.querySelectorAll('.focus-drag-row'));
var dragIdx = rows.indexOf(dragRow);
var targetIdx = rows.indexOf(row);
if (dragIdx < targetIdx) {
tbody.insertBefore(dragRow, row.nextSibling);
} else {
tbody.insertBefore(dragRow, row);
}
renumberFocusRows();
});
});
})();
</script>

View File

@@ -8,6 +8,9 @@
<div class="card">
{% for item in items %}
<div class="list-row">
{% with reorder_url="/links/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
<span class="row-title"><a href="{{ item.url }}" target="_blank">{{ item.label }}</a></span>
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}

View File

@@ -46,6 +46,9 @@
<div class="card mt-2">
{% for li in list_items %}
<div class="list-row {{ 'completed' if li.completed }}">
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=li.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.list_type == 'checklist' %}
<div class="row-check">
<form action="/lists/{{ item.id }}/items/{{ li.id }}/toggle" method="post" style="display:inline">
@@ -68,6 +71,9 @@
<!-- Child items -->
{% for child in child_map.get(li.id|string, []) %}
<div class="list-row {{ 'completed' if child.completed }}" style="padding-left: 48px;">
{% with reorder_url="/lists/" ~ item.id ~ "/items/reorder", item_id=child.id, extra_fields={"parent_id": li.id|string} %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.list_type == 'checklist' %}
<div class="row-check">
<form action="/lists/{{ item.id }}/items/{{ child.id }}/toggle" method="post" style="display:inline">

View File

@@ -25,6 +25,9 @@
<div class="card mt-3">
{% for item in items %}
<div class="list-row">
{% with reorder_url="/lists/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/lists/{{ item.id }}">{{ item.name }}</a></span>
<span class="row-meta">
{{ item.completed_count }}/{{ item.item_count }} items

View File

@@ -18,6 +18,9 @@
<div class="card mt-3">
{% for item in items %}
<div class="list-row">
{% with reorder_url="/meetings/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<span class="row-title"><a href="/meetings/{{ item.id }}">{{ item.title }}</a></span>
<span class="row-meta">{{ item.meeting_date }}</span>
{% if item.location %}

View File

@@ -19,7 +19,7 @@
</div>
</div>
{% if item.body %}
<div class="card"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body)">{{ item.body }}</div></div>
<div class="card" style="overflow:hidden"><div class="detail-body" style="white-space:pre-wrap;font-family:var(--font-body);margin-right:20px">{{ item.body }}</div></div>
{% else %}
<div class="card"><div class="text-muted" style="padding:20px">No content yet. <a href="/notes/{{ item.id }}/edit">Start writing</a></div></div>
{% endif %}

View File

@@ -8,6 +8,9 @@
<div class="card">
{% for item in items %}
<div class="list-row">
{% with reorder_url="/notes/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
{% if item.domain_color %}<span class="row-domain-tag" style="background:{{ item.domain_color }}22;color:{{ item.domain_color }}">{{ item.domain_name }}</span>{% endif %}
<span class="row-title"><a href="/notes/{{ item.id }}">{{ item.title }}</a></span>
{% if item.project_name %}<span class="row-tag">{{ item.project_name }}</span>{% endif %}

View File

@@ -0,0 +1,30 @@
{#
Reorder grip handle. Include with:
{% with reorder_url="/focus/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
Required context vars:
reorder_url - POST endpoint for the reorder action
item_id - ID of the current item
Optional:
extra_fields - dict of extra hidden fields (e.g. {"focus_date": "2026-03-03"})
#}
<span class="reorder-grip">
<form action="{{ reorder_url }}" method="post">
<input type="hidden" name="item_id" value="{{ item_id }}">
<input type="hidden" name="direction" value="up">
{% if extra_fields %}{% for k, v in extra_fields.items() %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}{% endif %}
<button type="submit" class="grip-btn" title="Move up"></button>
</form>
<form action="{{ reorder_url }}" method="post">
<input type="hidden" name="item_id" value="{{ item_id }}">
<input type="hidden" name="direction" value="down">
{% if extra_fields %}{% for k, v in extra_fields.items() %}
<input type="hidden" name="{{ k }}" value="{{ v }}">
{% endfor %}{% endif %}
<button type="submit" class="grip-btn" title="Move down"></button>
</form>
</span>

View File

@@ -23,6 +23,9 @@
{% endif %}
{% endif %}
<a href="/tasks/{{ item.id }}/edit" class="btn btn-secondary btn-sm">Edit</a>
<form action="/tasks/{{ item.id }}/delete" method="post" data-confirm="Delete this task?" style="display:inline">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
<button class="btn {{ 'btn-secondary' if item.status == 'done' else 'btn-primary' }} btn-sm">
{{ 'Reopen' if item.status == 'done' else 'Complete' }}

View File

@@ -116,4 +116,11 @@
</div>
</form>
</div>
{% if item %}
<div style="margin-top:12px;text-align:right;">
<form action="/tasks/{{ item.id }}/delete" method="post" data-confirm="Delete this task?" style="display:inline">
<button type="submit" class="btn btn-danger btn-sm">Delete Task</button>
</form>
</div>
{% endif %}
{% endblock %}

View File

@@ -13,6 +13,7 @@
<!-- Filters -->
<form class="filters-bar" method="get" action="/tasks">
<input type="text" name="search" value="{{ current_search }}" placeholder="Search tasks..." class="filter-select" style="min-width:160px">
<select name="status" class="filter-select" data-auto-submit onchange="this.form.submit()">
<option value="">All Statuses</option>
<option value="open" {{ 'selected' if current_status == 'open' }}>Open</option>
@@ -46,6 +47,10 @@
<option value="created_at" {{ 'selected' if current_sort == 'created_at' }}>Newest</option>
<option value="title" {{ 'selected' if current_sort == 'title' }}>Title</option>
</select>
<button type="submit" class="btn btn-ghost btn-xs">Search</button>
{% if current_search or current_domain_id or current_project_id or current_status or current_priority or current_context %}
<a href="/tasks" class="btn btn-ghost btn-xs" style="color:var(--red)">Clear</a>
{% endif %}
</form>
<!-- Task List -->
@@ -53,6 +58,9 @@
<div class="card">
{% for item in items %}
<div class="list-row {{ 'completed' if item.status in ['done', 'cancelled'] }} {{ 'timer-active' if running_task_id and item.id|string == running_task_id }}">
{% with reorder_url="/tasks/reorder", item_id=item.id %}
{% include 'partials/reorder_arrows.html' %}
{% endwith %}
<div class="row-check">
<form action="/tasks/{{ item.id }}/toggle" method="post" style="display:inline">
<input type="checkbox" id="check-{{ item.id }}" {{ 'checked' if item.status == 'done' }}

View File

@@ -67,7 +67,7 @@
{% for entry in entries %}
{% set entry_date = entry.start_at.strftime('%A, %B %-d') if entry.start_at else 'Unknown' %}
{% if entry_date != current_date.value %}
<div style="padding: 10px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
<div style="padding: 6px 12px 4px; font-size: 0.78rem; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; {% if not loop.first %}border-top: 1px solid var(--border); margin-top: 4px;{% endif %}">
{{ entry_date }}
</div>
{% set current_date.value = entry_date %}