Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aw-webui
9 changes: 6 additions & 3 deletions aw_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from .api import ServerAPI
from .custom_static import get_custom_static_blueprint
from .log import FlaskLogHandler
from .task_tracker import register as register_task_tracker

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -64,6 +65,9 @@ def __init__(
self.register_blueprint(rest.blueprint)
self.register_blueprint(get_custom_static_blueprint(custom_static))

# Register task tracker (creates tables + routes)
register_task_tracker(self)
Comment on lines +68 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Task tracker registered regardless of storage backend

register_task_tracker is called unconditionally, but models.py imports and uses _db from aw_datastore.storages.peewee. When storage_method is "memory" (the default and the one used in tests), Peewee storage is never initialised and _db is an uninitialised proxy. init_tables() will hit db_proxy.connect() on an unconfigured proxy and raise an OperationalError, crashing the entire server startup for every non-Peewee configuration.

Guard the registration so it only runs when the Peewee backend is in use, or make the task tracker initialise its own independent SQLite database.



class CustomJSONProvider(flask.json.provider.DefaultJSONProvider):
# encoding/decoding of datetime as iso8601 strings
Expand Down Expand Up @@ -101,9 +105,8 @@ def _config_cors(cors_origins: List[str], testing: bool):
"or CLI argument (could be a security risk): {}".format(cors_origins)
)

if testing:
# Used for development of aw-webui
cors_origins.append("http://127.0.0.1:27180/*")
# Allow Vue dev server (both testing and production/dev mode)
cors_origins.append("http://127.0.0.1:27180")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security CORS origin unconditionally added in production

http://127.0.0.1:27180 (the Vue dev server) is now appended on every startup, including production deployments. Previously this was guarded behind if testing:. Any page served from that dev-server origin can now make authenticated cross-origin requests to a production ActivityWatch instance at will.

Suggested change
cors_origins.append("http://127.0.0.1:27180")
if testing:
# Allow Vue dev server only during development
cors_origins.append("http://127.0.0.1:27180")


# TODO: This could probably be more specific
# See https://github.com/ActivityWatch/aw-server/pull/43#issuecomment-386888769
Expand Down
10 changes: 10 additions & 0 deletions aw_server/task_tracker/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Task Tracker sub-package — auto-registers blueprint and inits DB tables."""

from .models import init_tables
from .routes import bp as task_tracker_bp


def register(app):
"""Register the task tracker blueprint on the Flask app and create tables."""
init_tables()
app.register_blueprint(task_tracker_bp)
135 changes: 135 additions & 0 deletions aw_server/task_tracker/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
"""
Task Tracker models — Prisma schema ported to Peewee ORM.
Tables: Task, TimeEntry, AppUsage (+ Category enum)
"""

import peewee as pw
from datetime import datetime


def _ensure_dt(val):
"""Return a datetime object regardless of whether peewee returns str or datetime."""
if val is None:
return None
if isinstance(val, str):
# Peewee may return a stored string for timezone-aware datetimes
return datetime.fromisoformat(val)
return val

# Re-use the same peewee proxy that aw_datastore.storages.peewee uses.
# This way our models share the same SQLite database connection.
from aw_datastore.storages.peewee import _db as db_proxy
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Importing a private internal symbol from aw_datastore

_db is a private, implementation-internal variable (the underscore prefix signals this). Importing it from aw_datastore.storages.peewee tightly couples the task tracker to an internal detail that could change without warning in a future release of aw_datastore. Consider requesting that aw_datastore expose a stable public accessor, or maintain a separate SQLite connection owned by this sub-package.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!



class BaseModel(pw.Model):
"""Base model — all task-tracker models inherit from this."""

class Meta:
database = db_proxy


class Task(BaseModel):
id = pw.AutoField()
name = pw.CharField()
description = pw.CharField(null=True)
is_active = pw.BooleanField(default=False)
created_at = pw.DateTimeField(default=datetime.now)
updated_at = pw.DateTimeField(default=datetime.now)
Comment on lines +36 to +37
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 updated_at never refreshed on subsequent saves

updated_at uses default=datetime.now, which is evaluated once at object creation. Any subsequent task.save(), usage.save(), or template.save() call throughout routes.py leaves updated_at frozen at the original creation time. The same applies to TimeEntry (line 60) and Template (line 112).

Override save() on BaseModel to refresh updated_at automatically:

class BaseModel(pw.Model):
    def save(self, *args, **kwargs):
        if hasattr(self, "updated_at"):
            self.updated_at = datetime.now()
        return super().save(*args, **kwargs)


class Meta:
table_name = "task_tracker_task"
indexes = ()

def to_dict(self):
return {
"id": self.id,
"name": self.name,
"description": self.description,
"isActive": self.is_active,
"createdAt": _ensure_dt(self.created_at).isoformat(),
"updatedAt": _ensure_dt(self.updated_at).isoformat(),
}


class TimeEntry(BaseModel):
id = pw.AutoField()
task = pw.ForeignKeyField(Task, backref="timeEntries", on_delete="CASCADE")
start_time = pw.DateTimeField(default=datetime.now)
end_time = pw.DateTimeField(null=True)
created_at = pw.DateTimeField(default=datetime.now)
updated_at = pw.DateTimeField(default=datetime.now)

class Meta:
table_name = "task_tracker_timeentry"

def to_dict(self):
return {
"id": self.id,
"taskId": self.task_id,
"startTime": _ensure_dt(self.start_time).isoformat(),
"endTime": _ensure_dt(self.end_time).isoformat() if self.end_time else None,
"createdAt": _ensure_dt(self.created_at).isoformat(),
"updatedAt": _ensure_dt(self.updated_at).isoformat(),
}


class AppUsage(BaseModel):
id = pw.AutoField()
task = pw.ForeignKeyField(Task, backref="appUsages", on_delete="CASCADE")
app_name = pw.CharField()
title = pw.CharField(null=True)
total_seconds = pw.FloatField(default=0)
category = pw.CharField(default="NEUTRAL") # Category enum as string
created_at = pw.DateTimeField(default=datetime.now)
updated_at = pw.DateTimeField(default=datetime.now)

class Meta:
table_name = "task_tracker_appusage"
indexes = ((("task_id", "app_name"), True),) # unique(task_id, app_name)

def to_dict(self):
return {
"id": self.id,
"taskId": self.task_id,
"appName": self.app_name,
"title": self.title,
"totalSeconds": self.total_seconds,
"category": self.category,
"createdAt": _ensure_dt(self.created_at).isoformat(),
"updatedAt": _ensure_dt(self.updated_at).isoformat(),
}


class Template(BaseModel):
"""A template for quickly creating a new task with a preset category.
Can be general (task_id is NULL) or scoped to a specific task.
"""
id = pw.AutoField()
name = pw.CharField()
category = pw.CharField(default="PRODUCTIVE") # PRODUCTIVE / UNPRODUCTIVE / NEUTRAL
task = pw.ForeignKeyField(Task, null=True, backref="templates", on_delete="SET NULL")
created_at = pw.DateTimeField(default=datetime.now)
updated_at = pw.DateTimeField(default=datetime.now)

class Meta:
table_name = "task_tracker_template"
indexes = ()

def to_dict(self):
return {
"id": self.id,
"name": self.name,
"category": self.category,
"taskId": self.task_id,
"createdAt": _ensure_dt(self.created_at).isoformat(),
"updatedAt": _ensure_dt(self.updated_at).isoformat(),
}


def init_tables():
"""Create tables if they don't exist. Called once at server startup."""
# db_proxy (_db from aw_datastore.storages.peewee) is already initialized
# by the time this is called (PeeweeStorage.__init__ runs first).
if not db_proxy.is_connection_usable():
db_proxy.connect()
db_proxy.create_tables([Task, TimeEntry, AppUsage, Template], safe=True)
Loading