refactor: enhance TimerPage layout and functionality; update theme and tokens for improved design consistency

- Refactored TimerPage component to improve layout and user experience.
- Updated theme configuration to utilize design tokens for better maintainability.
- Introduced new tokens for colors and typography to align with design principles.
- Simplified state management and improved timer functionality.
- Enhanced accessibility and responsiveness of UI elements.
This commit is contained in:
timka123 committed 2026-10-18 23:57:36 +03:00
1 parent 17a0a3c4c9
commit 77b26757bf
84 files changed
+4501 -2682

No files matched your search

+3
View File
@@ -12,6 +12,9 @@ import tempfile
import pytest
# Before anything imports the settings: no `.env`, ever. A developer's real
# file would otherwise point the suite at live Jira, Telegram and laundry.
os.environ["KADENCE_NO_DOTENV"] = "1"
os.environ.setdefault("DATABASE_URL", "sqlite:///" + tempfile.mktemp(suffix=".db"))
os.environ["SCHEDULER_ENABLED"] = "false"
os.environ["TELEGRAM_BOT_ENABLED"] = "false"
+12 -3
View File
@@ -24,9 +24,6 @@ def test_a_whole_pomodoro_through_the_api(client):
assert done["finished"]["status"] == "completed"
assert done["completed_today"] == 1
marked = client.post(f"/api/tasks/{task['id']}/done", json={"done": True}).json()
assert marked["task"]["is_done"] is True
report = client.get("/api/reports/daily").json()
assert report["report"]["metrics"]["pomodoros"] == 1
assert "Daily report" in report["telegram_preview"]
@@ -37,6 +34,18 @@ def test_timer_errors_are_400_not_500(client):
assert client.post("/api/timer/complete", json={}).status_code == 400
def test_tasks_cannot_be_ticked_off_here(client):
"""Completion belongs to Jira / Todoist / Google — we only notice it.
405 rather than 404 because the SPA catch-all claims unknown paths for GET;
either way there is no way to close a task through the API.
"""
task = client.post("/api/tasks", json={"title": "Not yours to close"}).json()
response = client.post(f"/api/tasks/{task['id']}/done", json={"done": True})
assert response.status_code in (404, 405)
assert client.get("/api/tasks").json()["tasks"][0]["is_done"] is False
def test_task_crud(client):
created = client.post("/api/tasks", json={"title": "Temporary", "priority": "p2"}).json()
listing = client.get("/api/tasks").json()
+35
View File
@@ -60,3 +60,38 @@ def test_events_are_grouped_for_the_report(session, settings):
summary = blocker_service.events_for_day(session, settings)
assert summary["total"] == 4
assert summary["by_target"][0] == {"target": "youtube.com", "count": 3}
def test_masked_subdomains_are_expanded(session, settings):
"""A hosts file has no wildcards, so `*.x` has to become real names."""
settings.blocker.blocked_domains = ["*.google.com"]
settings.blocker.subdomain_expansions = ["www", "mail"]
hosts = blocker_service.expand_domains(settings)
assert hosts == ["google.com", "www.google.com", "mail.google.com"]
def test_allow_masks_carve_holes_in_block_masks(session, settings):
settings.blocker.blocked_domains = ["*.google.com", "reddit.com"]
settings.blocker.subdomain_expansions = ["www", "docs"]
settings.blocker.allowed_domains = ["docs.*", "m.reddit.com"]
hosts = blocker_service.expand_domains(settings)
assert "docs.google.com" not in hosts
assert "m.reddit.com" not in hosts
assert "www.google.com" in hosts and "reddit.com" in hosts
def test_patterns_a_hosts_file_cannot_express_are_reported(session, settings):
settings.blocker.blocked_domains = ["*gram*", "youtube.com", "*.x.com"]
assert blocker_service.unexpandable_masks(settings) == ["*gram*"]
hosts = blocker_service.expand_domains(settings)
assert "youtube.com" in hosts and "x.com" in hosts
def test_app_masks_are_passed_through_untouched(session, settings):
"""Process matching happens on the desktop, where globs work directly."""
settings.blocker.enabled = True
settings.blocker.mode = "always"
settings.blocker.blocked_apps = ["steam*", "*discord*"]
settings_service.save_settings(session, settings)
policy = blocker_service.policy(session, settings)
assert policy["apps"] == ["steam*", "*discord*"]
+9
View File
@@ -91,3 +91,12 @@ def test_reap_closes_intervals_left_running(session, settings):
assert pomodoro.reap_stale(session, settings) == 1
session.refresh(row)
assert row.status == "abandoned"
def test_timer_disabled_prevents_start(session, settings):
settings.pomodoro.timer_enabled = False
settings_service.save_settings(session, settings)
with pytest.raises(pomodoro.TimerError, match="view-only"):
pomodoro.start(session, settings, kind="work")
st = pomodoro.state(session, settings)
assert st["timer_enabled"] is False
+11 -3
View File
@@ -5,8 +5,16 @@ from __future__ import annotations
from datetime import timedelta
from app.models import utcnow
from app.services import pomodoro, report, settings_service, stats, tasks_service
from app.timeutil import local_date
from app.services import pomodoro, report, settings_service, stats, tasks_service
def _close(session, settings, task) -> None:
"""Simulate what a refresh does when a task has been closed in its own tool."""
task.is_done = True
task.completed_at = utcnow()
task.completed_date = local_date(settings.timezone)
session.commit()
def _run_pomodoros(session, settings, count: int, *, minutes: int = 25) -> None:
@@ -57,7 +65,7 @@ def test_report_payload_has_the_pieces_the_ui_draws(session, settings):
settings_service.save_settings(session, settings)
_run_pomodoros(session, settings, 2)
task = tasks_service.create_local_task(session, title="Ship the thing")
tasks_service.complete_task(session, settings, task.id)
_close(session, settings, task)
payload = report.build_daily(session, settings, local_date(settings.timezone))
assert payload["metrics"]["pomodoros"] == 2
@@ -69,7 +77,7 @@ def test_report_payload_has_the_pieces_the_ui_draws(session, settings):
def test_rendered_report_escapes_borrowed_text(session, settings):
task = tasks_service.create_local_task(session, title="Fix <b>&</b> ship")
tasks_service.complete_task(session, settings, task.id)
_close(session, settings, task)
payload = report.build_daily(session, settings, local_date(settings.timezone))
text = report.render_daily_telegram(payload, settings)
assert "Fix &lt;b&gt;&amp;&lt;/b&gt; ship" in text
+6 -12
View File
@@ -28,25 +28,19 @@ def test_stop_without_a_timer_explains_itself(session):
assert "No interval is running" in telegram_bot.handle_text("/stop")
def test_today_numbers_tasks_and_done_closes_them(session):
def test_today_lists_and_numbers_the_tasks(session):
settings_service.load_settings(session)
first = tasks_service.create_local_task(session, title="Alpha task")
tasks_service.create_local_task(session, title="Alpha task")
tasks_service.create_local_task(session, title="Beta task")
listing = telegram_bot.handle_text("/today")
assert "Alpha task" in listing and "<b>1.</b>" in listing
reply = telegram_bot.handle_text("/done 1")
assert "Alpha task" in reply
session.expire_all()
assert session.get(type(first), first.id).is_done is True
assert "No task with that number" in telegram_bot.handle_text("/done 99")
def test_done_without_a_listing_asks_for_one(session):
telegram_bot._INDEX.clear()
assert "/today" in telegram_bot.handle_text("/done 1")
def test_there_is_no_done_command(session):
"""Tasks are closed in their own tool; the bot must not pretend otherwise."""
assert telegram_bot.handle_text("/done 1") is None
assert "/done" not in telegram_bot.handle_text("/help")
def test_report_and_streak_render(session):