add blocker presets

This commit is contained in:
timka123 committed 2026-10-19 11:54:15 +03:00
1 parent 2be074f191
commit 772fdbfded
12 files changed
+426 -83

No files matched your search

+27 -2
View File
@@ -11,7 +11,7 @@ from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlalchemy import create_engine, inspect, text
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
from app.config import env_settings
@@ -32,11 +32,36 @@ engine = create_engine(env_settings().database_url, **_engine_kwargs(env_setting
SessionLocal = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
# Columns a refactor later dropped from the model but, with no migration tool
# in this project, left behind on any database that was already running —
# still `NOT NULL` with no server-side default, so every insert that no
# longer sets them fails. `create_all` only ever adds; this is the other half.
_DEAD_COLUMNS: dict[str, tuple[str, ...]] = {
"tasks": ("completed_here",),
}
def _drop_dead_columns() -> None:
inspector = inspect(engine)
existing_tables = set(inspector.get_table_names())
with engine.begin() as conn:
for table, columns in _DEAD_COLUMNS.items():
if table not in existing_tables:
continue
existing_columns = {c["name"] for c in inspector.get_columns(table)}
for column in columns:
if column in existing_columns:
conn.execute(text(f'ALTER TABLE {table} DROP COLUMN {column}'))
def init_db() -> None:
"""Create any missing tables. The schema is additive, so this is enough."""
"""Create any missing tables. The schema is additive, so this is enough —
plus the one-time cleanup above for columns a refactor removed from the
model."""
from app import models # noqa: F401 (register the mappers)
Base.metadata.create_all(bind=engine)
_drop_dead_columns()
@contextmanager
+19
View File
@@ -216,6 +216,25 @@ def extend(session: Session, minutes: int) -> PomodoroSession:
return row
def rescale_current(session: Session, settings: AppSettings) -> PomodoroSession | None:
"""Resize the running/paused interval to match a just-saved duration.
Without this, a preset (or any Focus/Short/Long edit) only takes effect
from the *next* interval — the one already ticking keeps whatever length
it was born with. `elapsed_seconds` is wall-clock, not stored, so this
only has to move `planned_seconds`; time already spent still counts, it
is just measured against the new total.
"""
row = current_session(session)
if row is None:
return None
planned = duration_for(settings, row.kind) # type: ignore[arg-type]
if planned != row.planned_seconds:
row.planned_seconds = planned
session.commit()
return row
def attach_task(session: Session, task_id: int | None) -> PomodoroSession:
row = _require_active(session)
if task_id is None:
+9 -1
View File
@@ -19,6 +19,7 @@ from sqlalchemy.orm import Session
from app.config import env_settings
from app.models import SettingsRow
from app.services import pomodoro
from app.settings_schema import SECRET_FIELDS, SECRET_MASK, AppSettings
_SETTINGS_ID = 1
@@ -193,4 +194,11 @@ def apply_patch(session: Session, patch: dict[str, Any]) -> AppSettings:
current = load_settings(session).model_dump(mode="json")
patch = unmask_secrets(current, patch)
merged = _merge(current, patch)
return save_settings(session, AppSettings.model_validate(merged))
updated = save_settings(session, AppSettings.model_validate(merged))
# A changed Focus/Short/Long minute (directly, or via a preset) resizes
# whatever interval is already running or paused — see
# `pomodoro.rescale_current`. Cheap and a no-op when nothing moved, so it
# is simplest to always run it rather than detect which keys changed.
pomodoro.rescale_current(session, updated)
return updated