2026-03-10 01:40:10 +03:00
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
from typing_extensions import Literal
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WorkDetail(BaseModel):
|
|
|
|
|
group_name: str
|
|
|
|
|
duration_seconds: float
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WorkSession(BaseModel):
|
|
|
|
|
start_time: datetime
|
|
|
|
|
end_time: datetime
|
|
|
|
|
details: List[WorkDetail]
|
|
|
|
|
synchronized: bool = False
|
2026-03-12 19:54:31 +03:00
|
|
|
id: Optional[str] = None
|
|
|
|
|
youtrack_created_at: Optional[datetime] = None
|
|
|
|
|
description: Optional[str] = None
|
|
|
|
|
youtrack_duration_minutes: Optional[float] = None
|
2026-03-12 18:50:39 +03:00
|
|
|
|
|
|
|
|
class Config:
|
|
|
|
|
json_encoders = {
|
|
|
|
|
datetime: lambda v: v.isoformat()
|
|
|
|
|
}
|
2026-03-10 01:40:10 +03:00
|
|
|
|
2026-03-12 19:54:31 +03:00
|
|
|
@property
|
|
|
|
|
def total_minutes(self) -> float:
|
|
|
|
|
return sum(d.duration_seconds for d in self.details) / 60.0
|
|
|
|
|
|
|
|
|
|
def is_modified(self) -> bool:
|
|
|
|
|
"""Проверяет, изменилась ли длительность сессии после последней синхронизации"""
|
|
|
|
|
if self.youtrack_duration_minutes is None:
|
|
|
|
|
return not self.synchronized # Несинхронизированная сессия = изменённая
|
|
|
|
|
return abs(self.total_minutes - self.youtrack_duration_minutes) > 0.5 # погрешность 30 сек
|
2026-03-10 01:40:10 +03:00
|
|
|
|
|
|
|
|
class TrackedTask(BaseModel):
|
|
|
|
|
task_id: str
|
|
|
|
|
project_id: Optional[str] = None
|
|
|
|
|
estimated_seconds: Optional[float] = None
|
|
|
|
|
start_time: datetime
|
|
|
|
|
end_time: Optional[datetime] = None
|
|
|
|
|
sessions: List[WorkSession] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def total_worked_seconds(self) -> float:
|
|
|
|
|
return sum(
|
|
|
|
|
detail.duration_seconds
|
|
|
|
|
for session in self.sessions
|
|
|
|
|
for detail in session.details
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def remaining_seconds(self) -> Optional[float]:
|
|
|
|
|
if self.estimated_seconds is None:
|
|
|
|
|
return None
|
|
|
|
|
return max(0.0, self.estimated_seconds - self.total_worked_seconds)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WindowPolicy(BaseModel):
|
|
|
|
|
group_name: str
|
|
|
|
|
window_patterns: List[str] = Field(default_factory=list)
|
|
|
|
|
process_names: List[str] = Field(default_factory=list)
|
|
|
|
|
policy_type: Literal["work", "blocked"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TrackerConfig(BaseModel):
|
|
|
|
|
tracking_enabled: bool = False
|
|
|
|
|
current_task_id: Optional[str] = None
|
|
|
|
|
window_policies: List[WindowPolicy] = Field(default_factory=list)
|
2026-03-12 19:41:29 +03:00
|
|
|
idle_threshold_seconds: int = 60 # Порог перехода в idle
|
|
|
|
|
new_session_after_idle_seconds: int = 600 # 10 минут для новой сессии
|
2026-03-10 01:40:10 +03:00
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def default_policies() -> List[WindowPolicy]:
|
|
|
|
|
return [
|
|
|
|
|
WindowPolicy(
|
|
|
|
|
group_name="IDE",
|
|
|
|
|
window_patterns=["PyCharm", "IntelliJ", "Visual Studio Code", "Code", "Sublime", "Vim", "Neovim"],
|
|
|
|
|
process_names=["pycharm64.exe", "idea64.exe", "code.exe", "sublime_text.exe", "nvim.exe", "vim.exe"],
|
|
|
|
|
policy_type="work"
|
|
|
|
|
),
|
|
|
|
|
WindowPolicy(
|
|
|
|
|
group_name="Gaming & Distractions",
|
|
|
|
|
window_patterns=["Steam", "Discord", "YouTube", "Twitch", "Spotify", "Netflix"],
|
|
|
|
|
process_names=["steam.exe", "discord.exe", "spotify.exe"],
|
|
|
|
|
policy_type="blocked"
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def __init__(self, **data):
|
|
|
|
|
super().__init__(**data)
|
|
|
|
|
if not self.window_policies:
|
2026-03-12 18:50:39 +03:00
|
|
|
self.window_policies = self.default_policies()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class YouTrackTimeTrackerEntryDuration(BaseModel):
|
|
|
|
|
minutes: int
|
|
|
|
|
|
|
|
|
|
class YouTrackTimeTrackerEntry(BaseModel):
|
|
|
|
|
id: str
|
|
|
|
|
duration: YouTrackTimeTrackerEntryDuration
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def minutes(self) -> int:
|
|
|
|
|
return self.duration.minutes
|