init
This commit is contained in:
121 files changed
+17641
No files matched your search
@@ -0,0 +1,183 @@
|
||||
"""Database tables.
|
||||
|
||||
The shape follows the three things the app records: **settings** (one JSON
|
||||
document), **what you did** (pomodoro sessions, task completions) and **what you
|
||||
were tempted by** (block events). Reports are derived from those and cached per
|
||||
day so a re-send shows the same numbers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
Float,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db import Base
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class SettingsRow(Base):
|
||||
"""The single settings document (`id = 1`), validated by `AppSettings`."""
|
||||
|
||||
__tablename__ = "settings"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, default=1)
|
||||
data: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=utcnow, onupdate=utcnow
|
||||
)
|
||||
|
||||
|
||||
class PomodoroSession(Base):
|
||||
"""One interval of the timer — work or break — as it actually happened.
|
||||
|
||||
`planned_seconds` is what the settings asked for, `actual_seconds` is what
|
||||
the clock recorded, and they differ whenever you finish early or abandon.
|
||||
"""
|
||||
|
||||
__tablename__ = "pomodoro_sessions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
kind: Mapped[str] = mapped_column(String(16), index=True) # work|short_break|long_break
|
||||
status: Mapped[str] = mapped_column(String(16), index=True, default="running")
|
||||
# running | paused | completed | abandoned
|
||||
|
||||
started_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=utcnow, index=True
|
||||
)
|
||||
ended_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
local_date: Mapped[date] = mapped_column(Date, index=True)
|
||||
"""The day this interval belongs to in the user's timezone — reports group on it."""
|
||||
|
||||
planned_seconds: Mapped[int] = mapped_column(Integer, default=1500)
|
||||
actual_seconds: Mapped[int] = mapped_column(Integer, default=0)
|
||||
paused_seconds: Mapped[int] = mapped_column(Integer, default=0)
|
||||
paused_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
interruptions: Mapped[int] = mapped_column(Integer, default=0)
|
||||
cycle_index: Mapped[int] = mapped_column(Integer, default=1)
|
||||
"""Position in the current long-break cycle (1..cycles_before_long_break)."""
|
||||
|
||||
task_source: Mapped[str | None] = mapped_column(String(32))
|
||||
task_external_id: Mapped[str | None] = mapped_column(String(128))
|
||||
task_title: Mapped[str | None] = mapped_column(Text)
|
||||
note: Mapped[str | None] = mapped_column(Text)
|
||||
abandon_reason: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
block_events: Mapped[list["BlockEvent"]] = relationship(back_populates="session")
|
||||
|
||||
@property
|
||||
def is_work(self) -> bool:
|
||||
return self.kind == "work"
|
||||
|
||||
|
||||
class TaskRecord(Base):
|
||||
"""A todo item, wherever it came from.
|
||||
|
||||
External tasks are cached here on every refresh so the UI is instant and the
|
||||
daily report still works when Jira is down. Locally created tasks live here
|
||||
too, with `source = "local"` — they are the only ones we own outright.
|
||||
"""
|
||||
|
||||
__tablename__ = "tasks"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("source", "external_id", name="uq_task_source_external"),
|
||||
Index("ix_task_source_done", "source", "is_done"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
source: Mapped[str] = mapped_column(String(32), index=True)
|
||||
external_id: Mapped[str] = mapped_column(String(128))
|
||||
|
||||
title: Mapped[str] = mapped_column(Text)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
url: Mapped[str | None] = mapped_column(Text)
|
||||
project: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[str | None] = mapped_column(String(64))
|
||||
priority: Mapped[str | None] = mapped_column(String(32))
|
||||
due_date: Mapped[date | None] = mapped_column(Date, index=True)
|
||||
start_date: Mapped[date | None] = mapped_column(Date)
|
||||
labels: Mapped[list[str]] = mapped_column(JSON, default=list)
|
||||
raw: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
|
||||
is_done: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
completed_date: Mapped[date | None] = mapped_column(Date, index=True)
|
||||
completed_here: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
"""True when *this app* closed it — what the daily report counts."""
|
||||
|
||||
estimated_pomodoros: Mapped[int | None] = mapped_column(Integer)
|
||||
pinned: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
"""Shown at the top of the timer's task picker."""
|
||||
|
||||
first_seen_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), default=utcnow
|
||||
)
|
||||
last_seen_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
stale: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
"""Set when a refresh no longer returns the task (closed elsewhere, or filtered)."""
|
||||
|
||||
|
||||
class BlockEvent(Base):
|
||||
"""A blocked site or app you reached for anyway. Pure gold for the report."""
|
||||
|
||||
__tablename__ = "block_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
kind: Mapped[str] = mapped_column(String(16), index=True) # domain | app
|
||||
target: Mapped[str] = mapped_column(String(255), index=True)
|
||||
at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, index=True)
|
||||
local_date: Mapped[date] = mapped_column(Date, index=True)
|
||||
action: Mapped[str] = mapped_column(String(16), default="blocked") # blocked|killed|warned
|
||||
details: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
session_id: Mapped[int | None] = mapped_column(ForeignKey("pomodoro_sessions.id"))
|
||||
session: Mapped[PomodoroSession | None] = relationship(back_populates="block_events")
|
||||
|
||||
|
||||
class DailyReport(Base):
|
||||
"""A generated report, kept so re-sends and the web view agree."""
|
||||
|
||||
__tablename__ = "daily_reports"
|
||||
__table_args__ = (UniqueConstraint("report_date", name="uq_report_date"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
report_date: Mapped[date] = mapped_column(Date, index=True)
|
||||
payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
generated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
sent_channel: Mapped[str | None] = mapped_column(String(32))
|
||||
score: Mapped[float] = mapped_column(Float, default=0.0)
|
||||
|
||||
|
||||
class EventLog(Base):
|
||||
"""Small append-only log used for "did we already send this?" questions.
|
||||
|
||||
Nudges, celebrations and briefings all need to be idempotent per day, and a
|
||||
row here is cheaper than a column per message type.
|
||||
"""
|
||||
|
||||
__tablename__ = "event_log"
|
||||
__table_args__ = (Index("ix_eventlog_kind_date", "kind", "local_date"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
kind: Mapped[str] = mapped_column(String(48), index=True)
|
||||
local_date: Mapped[date] = mapped_column(Date, index=True)
|
||||
at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow)
|
||||
payload: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict)
|
||||
Reference in new issue
Block a user