Team Hoa, EPIC R08 (UI/Application Separation) - Team Hoa scope only
(R08-T11 -> T14; R08-T01->T10 belong to Team Duy/Team Nam).
- R08-T11: ui/schedule_task_tab.py (795 lines) -> presentation/scheduling/
{kanban_board_widget,calendar_view_widget,ai_task_creator_dialog,
ai_task_import_dialog,run_history_dialog}.py + schedule_task_tab.py
shell. Kanban CRUD/drag-drop now goes through
application/scheduling/task_application_service.py (R07-T04) instead of
~30 lines of inline if/elif per drag target.
- R08-T12: ui/folder_tab.py (1587 lines, the largest of the four) ->
presentation/folder/{workspace_file_tree,document_preview_manager,
code_editor,office_document_renderer,ai_file_editor_dialog,
ai_edit_model_resolver,ai_edit_pipeline}.py + folder_tab.py shell.
Closes the R06-T05 loop: FileWorkspaceService existed since R06 with
zero production call sites (confirmed by grep); every plain-text write
(save/create/write_content) now goes through it, gaining path
containment and a Python-syntax warning the original code never had.
Pure helpers (_read_text, _is_probably_text, _pptx_available,
_split_code_block, _parse_ai_output) moved to
application/workspaces/{file_preview_helpers,ai_edit_output}.py.
- R08-T13: ui/dashboard_tab.py (437 lines) -> presentation/dashboard/
{token_usage_card_widget,usage_chart_widget,habits_widget}.py +
dashboard_tab.py shell, backed by a new
application/monitoring/dashboard_query_service.py (pricing/period/
summary queries the three widgets used to each recompute separately).
Directory-ownership note left in the checklist for Team Nam.
- R08-T14: ui/structure_graph_view.py (1035 lines) ->
presentation/graph/{graph_scene_items,graph_renderer,
graph_messages_view,graph_qa_widget}.py + structure_graph_view.py
shell. Extraction helpers (_pdf_to_markdown, _extract_file_contents)
moved to application/workspaces/graph_index_service.py (pure Python).
Renderer and Q&A panel talk only through signals
(node_selected/graph_rendered/raw_json_ready/project_changed) - neither
imports the other.
- presentation/shared/web_engine_support.py: HAS_WEB_ENGINE, previously
duplicated (folder_tab imported it FROM structure_graph_view.py) - now
one shared flag instead of one screen importing another screen's module.
All four old ui/*.py files deleted; app.py and ui/workspace_tab.py updated
to the new import paths (each god-file only had 1-2 real construction
sites, so import sites were updated directly rather than kept as a
strangler-fig shim - unlike core/tools.py at R05, which had dozens).
pytest: 377 pass (+94 vs the R07 baseline of 328; same 4 pre-existing
failures as the R05/R06 baseline, unrelated to this work).
scripts/check_imports.py: PASS. python -c "import cowork_local.app": OK.
Every new file < 400 lines (largest: graph_renderer.py, 391).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
111 lines
4.7 KiB
Python
111 lines
4.7 KiB
Python
"""DashboardQueryService - read-only usage/cost queries for the Dashboard
|
|
screen (R08-T13, extracted from ``ui/dashboard_tab.py::DashboardTab``, lines
|
|
196-199/256-261/284-322 of the original 437-line file: ``_pricing``,
|
|
``_period_range``'s date-math, and the ``period_totals``/``period_breakdown``
|
|
calls ``_refresh_chart`` made directly).
|
|
|
|
``ui/dashboard_tab.py`` called ``core/usage_tracker.py``/``core/model_
|
|
pricing.py`` directly from FIVE different methods spread across what is now
|
|
three widgets (``token_usage_card_widget.py``, ``usage_chart_widget.py``,
|
|
``habits_widget.py``) — each recomputing the same merged pricing dict. This
|
|
service is the one place that merge happens now; the three widgets share it
|
|
instead of each calling ``core.usage_tracker``/``core.model_pricing`` on
|
|
their own.
|
|
|
|
Pure Python: no Qt. Wraps ``core/usage_tracker.py`` (a plain-Python module
|
|
already) rather than reimplementing any of its date/cost math.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
from typing import Any, Dict, List, Tuple
|
|
|
|
|
|
class DashboardQueryService:
|
|
"""Usage/cost queries scoped to one ``AppContext``.
|
|
|
|
Args:
|
|
ctx: ``AppContext`` — read for ``ctx.config`` (pricing table,
|
|
currency, budget) and nothing else; this class does no I/O of
|
|
its own beyond what ``core.usage_tracker`` already does.
|
|
"""
|
|
|
|
def __init__(self, ctx: Any) -> None:
|
|
self.ctx = ctx
|
|
|
|
def pricing(self) -> Dict[str, Any]:
|
|
"""The merged price table (defaults + user overrides), synced from
|
|
Monitoring's model-pricing table first so cost figures always agree
|
|
between the two screens."""
|
|
from cowork_local.core import model_pricing as mp
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
mp.sync_to_usage(self.ctx.config)
|
|
return {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})}
|
|
|
|
def period_range(self, granularity: str, offset: int) -> Tuple[date, date]:
|
|
"""The SELECTED period as an inclusive ``(start, end)`` date range —
|
|
drives every widget on the screen (cards, chart, habits)."""
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
start, end = ut.period_bounds(granularity, offset)
|
|
return start, end - timedelta(days=1) # load_events end is inclusive
|
|
|
|
def summary(self, start: date, end: date) -> Dict[str, Any]:
|
|
"""Everything the stat cards + habits panel need for one period:
|
|
the raw events, ``usage_tracker.summarize``'s aggregate stats, the
|
|
per-bucket costs, and their total — computed once so both widgets
|
|
read the same numbers instead of loading events twice."""
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
events = ut.load_events(start, end)
|
|
pricing = self.pricing()
|
|
stats = ut.summarize(events)
|
|
costs = ut.cost_usd_events(events, pricing)
|
|
return {
|
|
"events": events,
|
|
"pricing": pricing,
|
|
"stats": stats,
|
|
"costs": costs,
|
|
"total_cost": sum(costs.values()),
|
|
}
|
|
|
|
def chart_series(self, granularity: str, offset: int, metric: str
|
|
) -> List[Tuple[str, float]]:
|
|
"""``(label, value)`` points for the spline chart — WEEK -> 7 days,
|
|
MONTH -> weeks, YEAR -> 12 months, in whichever ``metric``
|
|
("tokens" | "cost") was selected."""
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
events = ut.load_events() # all events; breakdown slices by period
|
|
pricing = self.pricing()
|
|
parts = ut.period_breakdown(events, granularity, pricing, offset=offset)
|
|
mi = 0 if metric == "tokens" else 1 # (label, tokens, cost) -> +1 for the value
|
|
return [(row[0], float(row[mi + 1])) for row in parts]
|
|
|
|
def period_totals(self, granularity: str, offset: int) -> Tuple[float, float]:
|
|
"""``(tokens, cost)`` totals for one period — used to compute the
|
|
vs-previous-period delta the chart's reference line shows."""
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
events = ut.load_events()
|
|
return ut.period_totals(events, granularity, self.pricing(), offset)
|
|
|
|
def period_range_label(self, granularity: str, offset: int) -> str:
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
return ut.period_range_label(granularity, offset)
|
|
|
|
def budget_status(self):
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
return ut.budget_status(self.ctx.config)
|
|
|
|
def set_budget(self, amount: float, currency: str) -> None:
|
|
from cowork_local.core import usage_tracker as ut
|
|
|
|
ut.set_budget(self.ctx.config, amount, currency)
|
|
|
|
|
|
__all__ = ["DashboardQueryService"]
|