diff --git a/frontend/src/views/ProjectListView.vue b/frontend/src/views/ProjectListView.vue index d21b3f3..f3ed7ad 100644 --- a/frontend/src/views/ProjectListView.vue +++ b/frontend/src/views/ProjectListView.vue @@ -20,7 +20,7 @@ interface Project { title: string; description: string | null; goal: string | null; - status: "active" | "completed" | "archived"; + status: "active" | "paused" | "completed" | "archived"; color: string | null; auto_summary: string | null; permission?: string; @@ -40,7 +40,7 @@ const toast = useToastStore(); const projects = ref([]); const loading = ref(false); const error = ref(null); -const activeTab = ref<"all" | "active" | "completed" | "archived">("all"); +const activeTab = ref<"all" | "active" | "paused" | "completed" | "archived">("all"); // New project modal const showNewProjectModal = ref(false); @@ -103,6 +103,7 @@ async function createProject() { function statusLabel(status: Project["status"]): string { if (status === "active") return "Active"; + if (status === "paused") return "Paused"; if (status === "completed") return "Completed"; if (status === "archived") return "Archived"; return status; diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 69f4f6a..83ab26a 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -23,7 +23,7 @@ interface Project { title: string; description: string | null; goal: string | null; - status: "active" | "completed" | "archived"; + status: "active" | "paused" | "completed" | "archived"; color: string | null; auto_summary: string | null; permission?: string; @@ -395,6 +395,7 @@ async function confirmDelete() { @@ -752,6 +753,7 @@ async function confirmDelete() { flex-shrink: 0; } .status-active { background: color-mix(in srgb, var(--color-success) 14%, transparent); color: var(--color-success); border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent); } +.status-paused { background: color-mix(in srgb, var(--color-accent-warm) 14%, transparent); color: var(--color-accent-warm); border: 1px solid color-mix(in srgb, var(--color-accent-warm) 30%, transparent); } .status-completed { background: color-mix(in srgb, var(--color-primary) 14%, transparent); color: var(--color-primary); border: 1px solid color-mix(in srgb, var(--color-primary) 30%, transparent); } .status-archived { background: color-mix(in srgb, var(--color-text-muted) 14%, transparent); color: var(--color-text-muted); border: 1px solid color-mix(in srgb, var(--color-text-muted) 30%, transparent); } diff --git a/src/fabledassistant/services/briefing_scheduler.py b/src/fabledassistant/services/briefing_scheduler.py index e69fec4..d38e388 100644 --- a/src/fabledassistant/services/briefing_scheduler.py +++ b/src/fabledassistant/services/briefing_scheduler.py @@ -162,6 +162,35 @@ def update_user_schedule(user_id: int, config: dict, tz_override: str | None = N # ── Job execution ───────────────────────────────────────────────────────────── +async def _auto_pause_stale_projects(user_id: int) -> list[str]: + """Pause active projects with no note/task activity in 14+ days. Returns paused project titles.""" + from sqlalchemy import select as _sel, func as _func + from fabledassistant.models.project import Project + from fabledassistant.models.note import Note + from fabledassistant.models import async_session as _session + + paused: list[str] = [] + threshold = datetime.now(timezone.utc) - __import__('datetime').timedelta(days=14) + try: + async with _session() as session: + projects = (await session.execute( + _sel(Project).where(Project.user_id == user_id, Project.status == "active") + )).scalars().all() + for p in projects: + latest = (await session.execute( + _sel(_func.max(Note.updated_at)).where(Note.project_id == p.id) + )).scalar() + if latest is None or latest < threshold: + p.status = "paused" + paused.append(p.title or "Untitled") + if paused: + await session.commit() + logger.info("Auto-paused %d stale projects for user %d: %s", len(paused), user_id, paused) + except Exception: + logger.debug("Auto-pause check failed for user %d", user_id, exc_info=True) + return paused + + async def _run_slot_for_user(user_id: int, slot: str) -> None: """Execute one slot job for one user.""" from fabledassistant.services.briefing_conversations import ( @@ -192,6 +221,9 @@ async def _run_slot_for_user(user_id: int, slot: str) -> None: model = await get_setting(user_id, "default_model", Config.OLLAMA_MODEL) if slot == "compilation": + # Auto-pause stale projects before compiling the briefing + auto_paused = await _auto_pause_stale_projects(user_id) + # Refresh external data first try: import json diff --git a/src/fabledassistant/services/notes.py b/src/fabledassistant/services/notes.py index cd0c492..2e2e052 100644 --- a/src/fabledassistant/services/notes.py +++ b/src/fabledassistant/services/notes.py @@ -22,6 +22,22 @@ def _normalize_tags(tags: list[str]) -> list[str]: return out +async def _maybe_reactivate_project(project_id: int) -> None: + """If a project is paused, reactivate it — activity indicates resumed work.""" + from fabledassistant.models.project import Project + try: + async with async_session() as session: + project = (await session.execute( + select(Project).where(Project.id == project_id) + )).scalars().first() + if project and project.status == "paused": + project.status = "active" + await session.commit() + logger.info("Auto-reactivated paused project %d (%s)", project_id, project.title) + except Exception: + logger.debug("_maybe_reactivate_project failed for project %d", project_id, exc_info=True) + + async def _maybe_trigger_project_summary(user_id: int, project_id: int) -> None: """Fire generate_project_summary() if the project summary is missing or >1h old.""" import asyncio @@ -92,6 +108,7 @@ async def create_note( await session.refresh(note) if project_id is not None: + await _maybe_reactivate_project(project_id) await _maybe_trigger_project_summary(user_id, project_id) return note @@ -284,6 +301,7 @@ async def update_note(user_id: int, note_id: int, **fields: object) -> Note | No await create_version(user_id, note_id, old_body, old_title, old_tags) if note.project_id is not None: + await _maybe_reactivate_project(note.project_id) await _maybe_trigger_project_summary(user_id, note.project_id) return note