-
-
Notifications
You must be signed in to change notification settings - Fork 84
Sumnic/update aw webui task tracker #161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| +211 −180 | package-lock.json | |
| +6 −0 | src/components/Header.vue | |
| +2 −0 | src/route.js | |
| +109 −0 | src/util/taskTrackerClient.ts | |
| +691 −0 | src/views/TaskTracker.vue | |
| +6 −0 | vue.config.js |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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__) | ||||||||||
|
|
||||||||||
|
|
@@ -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) | ||||||||||
|
|
||||||||||
|
|
||||||||||
| class CustomJSONProvider(flask.json.provider.DefaultJSONProvider): | ||||||||||
| # encoding/decoding of datetime as iso8601 strings | ||||||||||
|
|
@@ -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") | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| # TODO: This could probably be more specific | ||||||||||
| # See https://github.com/ActivityWatch/aw-server/pull/43#issuecomment-386888769 | ||||||||||
|
|
||||||||||
| 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) |
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Override 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) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
register_task_trackeris called unconditionally, butmodels.pyimports and uses_dbfromaw_datastore.storages.peewee. Whenstorage_methodis"memory"(the default and the one used in tests), Peewee storage is never initialised and_dbis an uninitialised proxy.init_tables()will hitdb_proxy.connect()on an unconfigured proxy and raise anOperationalError, 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.