From b0f8ad290574698e62f09bfcd21e9c392d984c25 Mon Sep 17 00:00:00 2001 From: Motzik20 Date: Sat, 28 Feb 2026 11:23:49 +0000 Subject: [PATCH 1/8] feat: add calender connections table with alembic migration --- .vscode/settings.json | 7 ++- ...5d12bc73_add_calendar_connections_table.py | 51 +++++++++++++++++++ backend/app/models/__init__.py | 2 + backend/app/models/calender_connections.py | 47 +++++++++++++++++ frontend/src/app/(dashboard)/page.tsx | 10 ++-- .../src/components/tasks/IngestionInput.tsx | 4 +- frontend/src/components/tasks/TaskList.tsx | 4 +- 7 files changed, 115 insertions(+), 10 deletions(-) create mode 100644 backend/app/alembic/versions/d8fd5d12bc73_add_calendar_connections_table.py create mode 100644 backend/app/models/calender_connections.py diff --git a/.vscode/settings.json b/.vscode/settings.json index db43021..250087d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -44,5 +44,10 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "python.analysis.typeCheckingMode": "strict", - "python.analysis.extraPaths": ["${workspaceFolder}/backend"] + "python.analysis.extraPaths": [ + "${workspaceFolder}/backend" + ], + "python-envs.defaultEnvManager": "ms-python.python:poetry", + "python-envs.pythonProjects": [], + "python-envs.defaultPackageManager": "ms-python.python:poetry" } diff --git a/backend/app/alembic/versions/d8fd5d12bc73_add_calendar_connections_table.py b/backend/app/alembic/versions/d8fd5d12bc73_add_calendar_connections_table.py new file mode 100644 index 0000000..938fb0f --- /dev/null +++ b/backend/app/alembic/versions/d8fd5d12bc73_add_calendar_connections_table.py @@ -0,0 +1,51 @@ +"""add calendar connections table + +Revision ID: d8fd5d12bc73 +Revises: fb4f1d257e34 +Create Date: 2026-02-28 11:21:31.921990 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = 'd8fd5d12bc73' +down_revision: Union[str, None] = 'fb4f1d257e34' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('calendar_connections', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('connection_type', sa.String(length=30), nullable=False), + sa.Column('label', sa.String(length=100), nullable=False), + sa.Column('calendar_url', sa.Text(), nullable=False), + sa.Column('username', sa.String(length=255), nullable=True), + sa.Column('key_id', sa.String(length=20), nullable=True), + sa.Column('secret', sa.Text(), nullable=True), + sa.Column('last_error', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('last_synced_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('last_error_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('secret_updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('calendar_connections') + # ### end Alembic commands ### diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 178ba26..ac12dee 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -5,6 +5,7 @@ from .temp_upload import TempUpload from .user import User from .user_setting import UserSetting +from .calender_connections import CalendarConnection __all__ = [ "Task", @@ -14,5 +15,6 @@ "ScheduleItem", "UserSetting", "TempUpload", + "CalendarConnection", "CeleryJob", ] diff --git a/backend/app/models/calender_connections.py b/backend/app/models/calender_connections.py new file mode 100644 index 0000000..12543ec --- /dev/null +++ b/backend/app/models/calender_connections.py @@ -0,0 +1,47 @@ +import datetime as dt +from typing import Any + +from pydantic import model_validator +from sqlalchemy import Text, func +from sqlmodel import Column, DateTime, Field, SQLModel, String, Boolean + +from app.core.timezone import convert_model_datetimes_to_utc, now_utc + + +class CalendarConnection(SQLModel, table=True): + __tablename__ = "calendar_connections" # type: ignore[assignment] + + id: int | None = Field(default=None, primary_key=True) + user_id: int = Field(foreign_key="users.id", nullable=False) + connection_type: str = Field(sa_column=Column(String(30), nullable=False)) # e.g., "ics_url", "caldav_basic" and other types of connections + label: str = Field(sa_column=Column(String(100), nullable=False)) # human readable name that can be assigne in the app + calendar_url: str = Field(sa_column=Column(Text, nullable=False)) + username: str | None = Field(sa_column=Column(String(255), nullable=True)) + key_id: str | None = Field(sa_column=Column(String(20), nullable=True)) + secret: str | None = Field(sa_column=Column(Text, nullable=True)) + last_error: str | None = Field(sa_column=Column(Text, nullable=True)) + is_active: bool = Field(default=True, sa_column=Column(Boolean, nullable=False)) # Disable connection without deleting it + last_synced_at: dt.datetime = Field( + default_factory=now_utc, + sa_column=Column(DateTime(timezone=True), server_default=func.now()), + ) + last_error_at: dt.datetime | None = Field( + sa_column=Column(DateTime(timezone=True), nullable=True), + ) + secret_updated_at: dt.datetime | None = Field( + sa_column=Column(DateTime(timezone=True), nullable=True), + ) + created_at: dt.datetime = Field( + default_factory=now_utc, + sa_column=Column(DateTime(timezone=True), server_default=func.now()), + ) + updated_at: dt.datetime = Field( + default_factory=now_utc, + sa_column=Column(DateTime(timezone=True), server_default=func.now()), + ) + + @model_validator(mode="before") + @classmethod + def convert_datetimes_to_utc(cls, data: dict[str, Any]) -> dict[str, Any]: + """Convert all datetime fields to UTC before validation.""" + return convert_model_datetimes_to_utc(data) diff --git a/frontend/src/app/(dashboard)/page.tsx b/frontend/src/app/(dashboard)/page.tsx index 1861af4..923ba24 100644 --- a/frontend/src/app/(dashboard)/page.tsx +++ b/frontend/src/app/(dashboard)/page.tsx @@ -23,7 +23,7 @@ import { export default function Home() { return ( -
+
{/* Hero Section */} @@ -32,7 +32,7 @@ export default function Home() { AI-Powered Task Management
-

+

Welcome to Chrono Guide

@@ -43,7 +43,7 @@ export default function Home() { {/* Quick Actions */}

- +
@@ -72,7 +72,7 @@ export default function Home() { - +
@@ -101,7 +101,7 @@ export default function Home() { - +
diff --git a/frontend/src/components/tasks/IngestionInput.tsx b/frontend/src/components/tasks/IngestionInput.tsx index 4c56a33..2d4e94a 100644 --- a/frontend/src/components/tasks/IngestionInput.tsx +++ b/frontend/src/components/tasks/IngestionInput.tsx @@ -285,11 +285,11 @@ function TextInput({ Input Text -
+