diff --git a/backend/app/api/v1/dashboard.py b/backend/app/api/v1/dashboard.py new file mode 100644 index 00000000..c53920b0 --- /dev/null +++ b/backend/app/api/v1/dashboard.py @@ -0,0 +1,51 @@ +import uuid +from datetime import date + +from fastapi import APIRouter, Depends +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_db_session, require_study_member +from app.models.milestone import Milestone +from app.models.task import Task +from app.schemas.progress import StudyProgressRead + +router = APIRouter() + + +@router.get("/progress", response_model=StudyProgressRead, dependencies=[Depends(require_study_member())]) +async def get_progress( + study_id: uuid.UUID, + db: AsyncSession = Depends(get_db_session), +) -> StudyProgressRead: + # Milestones + milestone_total_stmt = select(func.count()).select_from(Milestone).where(Milestone.study_id == study_id) + milestone_done_stmt = select(func.count()).select_from(Milestone).where( + Milestone.study_id == study_id, Milestone.status == "DONE" + ) + task_total_stmt = select(func.count()).select_from(Task).where(Task.study_id == study_id) + task_done_stmt = select(func.count()).select_from(Task).where(Task.study_id == study_id, Task.status == "DONE") + task_overdue_stmt = select(func.count()).select_from(Task).where( + Task.study_id == study_id, + Task.due_date < date.today(), + Task.status != "DONE", + ) + + milestone_total = (await db.execute(milestone_total_stmt)).scalar_one() + milestone_done = (await db.execute(milestone_done_stmt)).scalar_one() + task_total = (await db.execute(task_total_stmt)).scalar_one() + task_done = (await db.execute(task_done_stmt)).scalar_one() + task_overdue = (await db.execute(task_overdue_stmt)).scalar_one() + + total_items = milestone_total + task_total + done_items = milestone_done + task_done + completion_rate = float(done_items / total_items) if total_items else 0.0 + + return StudyProgressRead( + milestones_total=milestone_total, + milestones_done=milestone_done, + tasks_total=task_total, + tasks_done=task_done, + tasks_overdue=task_overdue, + completion_rate=completion_rate, + ) diff --git a/backend/app/api/v1/milestones.py b/backend/app/api/v1/milestones.py new file mode 100644 index 00000000..ba6ffb60 --- /dev/null +++ b/backend/app/api/v1/milestones.py @@ -0,0 +1,94 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles +from app.crud import audit as audit_crud +from app.crud import milestone as milestone_crud +from app.crud import study as study_crud +from app.schemas.milestone import MilestoneCreate, MilestoneRead, MilestoneUpdate + +router = APIRouter() + + +async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): + study = await study_crud.get(db, study_id) + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + return study + + +@router.post( + "/", + response_model=MilestoneRead, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_study_roles(["PM"]))], +) +async def create_milestone( + study_id: uuid.UUID, + milestone_in: MilestoneCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> MilestoneRead: + await _ensure_study_exists(db, study_id) + milestone = await milestone_crud.create(db, study_id, milestone_in) + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="milestone", + entity_id=milestone.id, + action="CREATE_MILESTONE", + detail=f"Milestone {milestone.id} created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return milestone + + +@router.get( + "/", + response_model=list[MilestoneRead], + dependencies=[Depends(require_study_member())], +) +async def list_milestones( + study_id: uuid.UUID, + db: AsyncSession = Depends(get_db_session), +) -> list[MilestoneRead]: + await _ensure_study_exists(db, study_id) + milestones = await milestone_crud.list_milestones(db, study_id) + return list(milestones) + + +@router.patch( + "/{milestone_id}", + response_model=MilestoneRead, + dependencies=[Depends(require_study_roles(["PM"]))], +) +async def update_milestone( + study_id: uuid.UUID, + milestone_id: uuid.UUID, + milestone_in: MilestoneUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> MilestoneRead: + await _ensure_study_exists(db, study_id) + milestone = await milestone_crud.get(db, milestone_id) + if not milestone or milestone.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Milestone not found") + old_status = milestone.status + updated = await milestone_crud.update(db, milestone, milestone_in) + detail = None + if milestone_in.status and milestone_in.status != old_status: + detail = f"milestone {milestone_id} status {old_status} -> {milestone_in.status}" + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="milestone", + entity_id=milestone_id, + action="UPDATE_MILESTONE", + detail=detail or "Milestone updated", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return updated diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 45cc9979..de9cf9a6 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,6 +1,6 @@ from fastapi import APIRouter -from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs +from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -11,3 +11,6 @@ api_router.include_router(members.router, prefix="/studies/{study_id}/members", api_router.include_router(comments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/comments", tags=["comments"]) api_router.include_router(attachments.router, prefix="/studies/{study_id}/{entity_type}/{entity_id}/attachments", tags=["attachments"]) api_router.include_router(audit_logs.router, prefix="/studies/{study_id}/audit-logs", tags=["audit-logs"]) +api_router.include_router(milestones.router, prefix="/studies/{study_id}/milestones", tags=["milestones"]) +api_router.include_router(tasks.router, prefix="/studies/{study_id}/tasks", tags=["tasks"]) +api_router.include_router(dashboard.router, prefix="/studies/{study_id}/dashboard", tags=["dashboard"]) diff --git a/backend/app/api/v1/tasks.py b/backend/app/api/v1/tasks.py new file mode 100644 index 00000000..5cead312 --- /dev/null +++ b/backend/app/api/v1/tasks.py @@ -0,0 +1,114 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.deps import get_current_user, get_db_session, require_study_member, require_study_roles +from app.crud import audit as audit_crud +from app.crud import milestone as milestone_crud +from app.crud import study as study_crud +from app.crud import task as task_crud +from app.schemas.task import TaskCreate, TaskRead, TaskUpdate + +router = APIRouter() + +ALLOW_ASSIGNEE_UPDATE_STATUS = False + + +async def _ensure_study_exists(db: AsyncSession, study_id: uuid.UUID): + study = await study_crud.get(db, study_id) + if not study: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Study not found") + return study + + +async def _validate_milestone(db: AsyncSession, study_id: uuid.UUID, milestone_id: uuid.UUID | None): + if milestone_id is None: + return + milestone = await milestone_crud.get(db, milestone_id) + if not milestone: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Milestone not found") + if milestone.study_id != study_id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Milestone does not belong to study") + + +@router.post( + "/", + response_model=TaskRead, + status_code=status.HTTP_201_CREATED, + dependencies=[Depends(require_study_roles(["PM"]))], +) +async def create_task( + study_id: uuid.UUID, + task_in: TaskCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> TaskRead: + await _ensure_study_exists(db, study_id) + await _validate_milestone(db, study_id, task_in.milestone_id) + task = await task_crud.create(db, study_id, task_in, created_by=current_user.id) + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="task", + entity_id=task.id, + action="CREATE_TASK", + detail=f"Task {task.id} created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return task + + +@router.get( + "/", + response_model=list[TaskRead], + dependencies=[Depends(require_study_member())], +) +async def list_tasks( + study_id: uuid.UUID, + milestone_id: uuid.UUID | None = None, + assignee_id: uuid.UUID | None = None, + status: str | None = None, + db: AsyncSession = Depends(get_db_session), +) -> list[TaskRead]: + await _ensure_study_exists(db, study_id) + tasks = await task_crud.list_tasks(db, study_id, milestone_id=milestone_id, assignee_id=assignee_id, status=status) + return list(tasks) + + +@router.patch( + "/{task_id}", + response_model=TaskRead, + dependencies=[Depends(require_study_roles(["PM"]))], +) +async def update_task( + study_id: uuid.UUID, + task_id: uuid.UUID, + task_in: TaskUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> TaskRead: + await _ensure_study_exists(db, study_id) + task = await task_crud.get(db, task_id) + if not task or task.study_id != study_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Task not found") + if task_in.milestone_id: + await _validate_milestone(db, study_id, task_in.milestone_id) + + old_status = task.status + updated = await task_crud.update(db, task, task_in) + detail = None + if task_in.status and task_in.status != old_status: + detail = f"task {task_id} status {old_status} -> {task_in.status}" + await audit_crud.log_action( + db, + study_id=study_id, + entity_type="task", + entity_id=task_id, + action="UPDATE_TASK" if not detail else "TASK_STATUS_CHANGE", + detail=detail or "Task updated", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return updated diff --git a/backend/app/crud/milestone.py b/backend/app/crud/milestone.py new file mode 100644 index 00000000..3cec213d --- /dev/null +++ b/backend/app/crud/milestone.py @@ -0,0 +1,48 @@ +import uuid +from typing import Sequence + +from sqlalchemy import select, update as sa_update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.milestone import Milestone +from app.schemas.milestone import MilestoneCreate, MilestoneUpdate + + +async def create(db: AsyncSession, study_id: uuid.UUID, milestone_in: MilestoneCreate) -> Milestone: + milestone = Milestone( + study_id=study_id, + type=milestone_in.type, + name=milestone_in.name or milestone_in.type, + planned_date=milestone_in.planned_date, + actual_date=None, + status=milestone_in.status or "NOT_STARTED", + owner_id=milestone_in.owner_id, + notes=milestone_in.notes, + ) + db.add(milestone) + await db.commit() + await db.refresh(milestone) + return milestone + + +async def get(db: AsyncSession, milestone_id: uuid.UUID) -> Milestone | None: + result = await db.execute(select(Milestone).where(Milestone.id == milestone_id)) + return result.scalar_one_or_none() + + +async def list_milestones(db: AsyncSession, study_id: uuid.UUID) -> Sequence[Milestone]: + result = await db.execute(select(Milestone).where(Milestone.study_id == study_id).order_by(Milestone.planned_date)) + return result.scalars().all() + + +async def update(db: AsyncSession, milestone: Milestone, milestone_in: MilestoneUpdate) -> Milestone: + update_data = milestone_in.model_dump(exclude_unset=True) + if update_data: + await db.execute( + sa_update(Milestone) + .where(Milestone.id == milestone.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(milestone) + return milestone diff --git a/backend/app/crud/task.py b/backend/app/crud/task.py new file mode 100644 index 00000000..9e8ab977 --- /dev/null +++ b/backend/app/crud/task.py @@ -0,0 +1,77 @@ +import uuid +from datetime import datetime, timezone +from typing import Sequence + +from sqlalchemy import select, update as sa_update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.task import Task +from app.schemas.task import TaskCreate, TaskUpdate + + +async def create( + db: AsyncSession, + study_id: uuid.UUID, + task_in: TaskCreate, + *, + created_by: uuid.UUID, +) -> Task: + task = Task( + study_id=study_id, + milestone_id=task_in.milestone_id, + title=task_in.title, + description=task_in.description, + assignee_id=task_in.assignee_id, + priority=task_in.priority, + due_date=task_in.due_date, + status="TODO", + created_by=created_by, + ) + db.add(task) + await db.commit() + await db.refresh(task) + return task + + +async def get(db: AsyncSession, task_id: uuid.UUID) -> Task | None: + result = await db.execute(select(Task).where(Task.id == task_id)) + return result.scalar_one_or_none() + + +async def list_tasks( + db: AsyncSession, + study_id: uuid.UUID, + milestone_id: uuid.UUID | None = None, + assignee_id: uuid.UUID | None = None, + status: str | None = None, +) -> Sequence[Task]: + stmt = select(Task).where(Task.study_id == study_id) + if milestone_id: + stmt = stmt.where(Task.milestone_id == milestone_id) + if assignee_id: + stmt = stmt.where(Task.assignee_id == assignee_id) + if status: + stmt = stmt.where(Task.status == status) + stmt = stmt.order_by(Task.due_date) + result = await db.execute(stmt) + return result.scalars().all() + + +async def update(db: AsyncSession, task: Task, task_in: TaskUpdate) -> Task: + update_data = task_in.model_dump(exclude_unset=True) + if "status" in update_data: + status_change = update_data["status"] + if status_change == "DONE": + update_data["completed_at"] = datetime.now(timezone.utc) + else: + update_data["completed_at"] = None + + if update_data: + await db.execute( + sa_update(Task) + .where(Task.id == task.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(task) + return task diff --git a/backend/app/db/base.py b/backend/app/db/base.py index f8963b28..71a6b635 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -8,3 +8,5 @@ from app.models.study_member import StudyMember # noqa: F401 from app.models.comment import Comment # noqa: F401 from app.models.attachment import Attachment # noqa: F401 from app.models.audit_log import AuditLog # noqa: F401 +from app.models.milestone import Milestone # noqa: F401 +from app.models.task import Task # noqa: F401 diff --git a/backend/app/models/milestone.py b/backend/app/models/milestone.py new file mode 100644 index 00000000..ec8030f3 --- /dev/null +++ b/backend/app/models/milestone.py @@ -0,0 +1,26 @@ +import uuid +from datetime import datetime, date + +from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class Milestone(Base): + __tablename__ = "milestones" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) + type: Mapped[str] = mapped_column(String(50), nullable=False) + name: Mapped[str] = mapped_column(String(200), nullable=False) + planned_date: Mapped[date | None] = mapped_column(Date, nullable=True) + actual_date: Mapped[date | None] = mapped_column(Date, nullable=True) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="NOT_STARTED") + owner_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + notes: Mapped[str | None] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() + ) diff --git a/backend/app/models/task.py b/backend/app/models/task.py new file mode 100644 index 00000000..3b35e2df --- /dev/null +++ b/backend/app/models/task.py @@ -0,0 +1,28 @@ +import uuid +from datetime import datetime, date + +from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class Task(Base): + __tablename__ = "tasks" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), index=True, nullable=False) + milestone_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("milestones.id"), nullable=True) + title: Mapped[str] = mapped_column(String(200), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + assignee_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) + priority: Mapped[str] = mapped_column(String(20), nullable=False, default="MEDIUM") + due_date: Mapped[date | None] = mapped_column(Date, nullable=True) + status: Mapped[str] = mapped_column(String(20), nullable=False, default="TODO") + completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now() + ) diff --git a/backend/app/schemas/milestone.py b/backend/app/schemas/milestone.py new file mode 100644 index 00000000..8a711a7e --- /dev/null +++ b/backend/app/schemas/milestone.py @@ -0,0 +1,39 @@ +import uuid +from datetime import date, datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class MilestoneCreate(BaseModel): + type: str + name: Optional[str] = None + planned_date: Optional[date] = None + owner_id: Optional[uuid.UUID] = None + notes: Optional[str] = None + status: str = Field(default="NOT_STARTED") + + +class MilestoneUpdate(BaseModel): + name: Optional[str] = None + planned_date: Optional[date] = None + actual_date: Optional[date] = None + status: Optional[str] = None + owner_id: Optional[uuid.UUID] = None + notes: Optional[str] = None + + +class MilestoneRead(BaseModel): + id: uuid.UUID + study_id: uuid.UUID + type: str + name: str + planned_date: Optional[date] + actual_date: Optional[date] + status: str + owner_id: Optional[uuid.UUID] + notes: Optional[str] + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/schemas/progress.py b/backend/app/schemas/progress.py new file mode 100644 index 00000000..31028325 --- /dev/null +++ b/backend/app/schemas/progress.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel + + +class StudyProgressRead(BaseModel): + milestones_total: int + milestones_done: int + tasks_total: int + tasks_done: int + tasks_overdue: int + completion_rate: float diff --git a/backend/app/schemas/task.py b/backend/app/schemas/task.py new file mode 100644 index 00000000..6816fb9a --- /dev/null +++ b/backend/app/schemas/task.py @@ -0,0 +1,42 @@ +import uuid +from datetime import date, datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class TaskCreate(BaseModel): + milestone_id: Optional[uuid.UUID] = None + title: str + description: Optional[str] = None + assignee_id: Optional[uuid.UUID] = None + priority: str = "MEDIUM" + due_date: Optional[date] = None + + +class TaskUpdate(BaseModel): + milestone_id: Optional[uuid.UUID] = None + title: Optional[str] = None + description: Optional[str] = None + assignee_id: Optional[uuid.UUID] = None + priority: Optional[str] = None + due_date: Optional[date] = None + status: Optional[str] = None + + +class TaskRead(BaseModel): + id: uuid.UUID + study_id: uuid.UUID + milestone_id: Optional[uuid.UUID] + title: str + description: Optional[str] + assignee_id: Optional[uuid.UUID] + priority: str + due_date: Optional[date] + status: str + completed_at: Optional[datetime] + created_by: uuid.UUID + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) diff --git a/pg_data/base/16384/1247 b/pg_data/base/16384/1247 index 3f84f3d6..f8480d63 100644 Binary files a/pg_data/base/16384/1247 and b/pg_data/base/16384/1247 differ diff --git a/pg_data/base/16384/1249 b/pg_data/base/16384/1249 index fec67a04..1d3faed6 100644 Binary files a/pg_data/base/16384/1249 and b/pg_data/base/16384/1249 differ diff --git a/pg_data/base/16384/1259 b/pg_data/base/16384/1259 index 9ae22a67..9e639dd9 100644 Binary files a/pg_data/base/16384/1259 and b/pg_data/base/16384/1259 differ diff --git a/pg_data/base/16384/24576 b/pg_data/base/16384/24576 index 64aea0dc..1250058c 100644 Binary files a/pg_data/base/16384/24576 and b/pg_data/base/16384/24576 differ diff --git a/pg_data/base/16384/24581 b/pg_data/base/16384/24581 index 8d1c65e9..5bf27cfd 100644 Binary files a/pg_data/base/16384/24581 and b/pg_data/base/16384/24581 differ diff --git a/pg_data/base/16384/24583 b/pg_data/base/16384/24583 index 6de5b9c6..d4a695ed 100644 Binary files a/pg_data/base/16384/24583 and b/pg_data/base/16384/24583 differ diff --git a/pg_data/base/16384/24584 b/pg_data/base/16384/24584 index 0b70a971..a6c7a595 100644 Binary files a/pg_data/base/16384/24584 and b/pg_data/base/16384/24584 differ diff --git a/pg_data/base/16384/24590 b/pg_data/base/16384/24590 index d4826e9b..0fdee861 100644 Binary files a/pg_data/base/16384/24590 and b/pg_data/base/16384/24590 differ diff --git a/pg_data/base/16384/24597 b/pg_data/base/16384/24597 index 7df4c798..53d093e0 100644 Binary files a/pg_data/base/16384/24597 and b/pg_data/base/16384/24597 differ diff --git a/pg_data/base/16384/24613 b/pg_data/base/16384/24613 index e69de29b..f3bda7cf 100644 Binary files a/pg_data/base/16384/24613 and b/pg_data/base/16384/24613 differ diff --git a/pg_data/base/16384/24618 b/pg_data/base/16384/24618 index 314573e5..08f3afb3 100644 Binary files a/pg_data/base/16384/24618 and b/pg_data/base/16384/24618 differ diff --git a/pg_data/base/16384/24620 b/pg_data/base/16384/24620 index 3c8df0fc..56a82382 100644 Binary files a/pg_data/base/16384/24620 and b/pg_data/base/16384/24620 differ diff --git a/pg_data/base/16384/24632 b/pg_data/base/16384/24632 index ef32a783..541b050c 100644 Binary files a/pg_data/base/16384/24632 and b/pg_data/base/16384/24632 differ diff --git a/pg_data/base/16384/24672 b/pg_data/base/16384/24672 index d5c2f14e..f69e66b7 100644 Binary files a/pg_data/base/16384/24672 and b/pg_data/base/16384/24672 differ diff --git a/pg_data/base/16384/24678 b/pg_data/base/16384/24678 index 6e1c6851..a449fd6a 100644 Binary files a/pg_data/base/16384/24678 and b/pg_data/base/16384/24678 differ diff --git a/pg_data/base/16384/24690 b/pg_data/base/16384/24690 new file mode 100644 index 00000000..af43b39b Binary files /dev/null and b/pg_data/base/16384/24690 differ diff --git a/pg_data/base/16384/24695 b/pg_data/base/16384/24695 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/24696 b/pg_data/base/16384/24696 new file mode 100644 index 00000000..f2a4f51a Binary files /dev/null and b/pg_data/base/16384/24696 differ diff --git a/pg_data/base/16384/24697 b/pg_data/base/16384/24697 new file mode 100644 index 00000000..9d8db47a Binary files /dev/null and b/pg_data/base/16384/24697 differ diff --git a/pg_data/base/16384/24709 b/pg_data/base/16384/24709 new file mode 100644 index 00000000..7f3fa2dd Binary files /dev/null and b/pg_data/base/16384/24709 differ diff --git a/pg_data/base/16384/24710 b/pg_data/base/16384/24710 new file mode 100644 index 00000000..b4c206d8 Binary files /dev/null and b/pg_data/base/16384/24710 differ diff --git a/pg_data/base/16384/24715 b/pg_data/base/16384/24715 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/24716 b/pg_data/base/16384/24716 new file mode 100644 index 00000000..91056c37 Binary files /dev/null and b/pg_data/base/16384/24716 differ diff --git a/pg_data/base/16384/24717 b/pg_data/base/16384/24717 new file mode 100644 index 00000000..273c077b Binary files /dev/null and b/pg_data/base/16384/24717 differ diff --git a/pg_data/base/16384/24739 b/pg_data/base/16384/24739 new file mode 100644 index 00000000..226a307b Binary files /dev/null and b/pg_data/base/16384/24739 differ diff --git a/pg_data/base/16384/2579 b/pg_data/base/16384/2579 index bc92f521..011f9bef 100644 Binary files a/pg_data/base/16384/2579 and b/pg_data/base/16384/2579 differ diff --git a/pg_data/base/16384/2604 b/pg_data/base/16384/2604 index 2003d687..d2397577 100644 Binary files a/pg_data/base/16384/2604 and b/pg_data/base/16384/2604 differ diff --git a/pg_data/base/16384/2606 b/pg_data/base/16384/2606 index 29dd1e2a..43156c23 100644 Binary files a/pg_data/base/16384/2606 and b/pg_data/base/16384/2606 differ diff --git a/pg_data/base/16384/2606_fsm b/pg_data/base/16384/2606_fsm index db7d7151..9aabdda8 100644 Binary files a/pg_data/base/16384/2606_fsm and b/pg_data/base/16384/2606_fsm differ diff --git a/pg_data/base/16384/2608 b/pg_data/base/16384/2608 index b846db0a..bd0ac36f 100644 Binary files a/pg_data/base/16384/2608 and b/pg_data/base/16384/2608 differ diff --git a/pg_data/base/16384/2610 b/pg_data/base/16384/2610 index 47353b16..09ef1dd4 100644 Binary files a/pg_data/base/16384/2610 and b/pg_data/base/16384/2610 differ diff --git a/pg_data/base/16384/2619 b/pg_data/base/16384/2619 index 4961cd71..3f1ad759 100644 Binary files a/pg_data/base/16384/2619 and b/pg_data/base/16384/2619 differ diff --git a/pg_data/base/16384/2619_fsm b/pg_data/base/16384/2619_fsm index 63545ae7..311639de 100644 Binary files a/pg_data/base/16384/2619_fsm and b/pg_data/base/16384/2619_fsm differ diff --git a/pg_data/base/16384/2620 b/pg_data/base/16384/2620 index 701812e9..63585763 100644 Binary files a/pg_data/base/16384/2620 and b/pg_data/base/16384/2620 differ diff --git a/pg_data/base/16384/2620_fsm b/pg_data/base/16384/2620_fsm new file mode 100644 index 00000000..0bf0123b Binary files /dev/null and b/pg_data/base/16384/2620_fsm differ diff --git a/pg_data/base/16384/2656 b/pg_data/base/16384/2656 index 3e55a033..a9295371 100644 Binary files a/pg_data/base/16384/2656 and b/pg_data/base/16384/2656 differ diff --git a/pg_data/base/16384/2657 b/pg_data/base/16384/2657 index 591b6232..daa57351 100644 Binary files a/pg_data/base/16384/2657 and b/pg_data/base/16384/2657 differ diff --git a/pg_data/base/16384/2658 b/pg_data/base/16384/2658 index e1244a12..7e47dc80 100644 Binary files a/pg_data/base/16384/2658 and b/pg_data/base/16384/2658 differ diff --git a/pg_data/base/16384/2659 b/pg_data/base/16384/2659 index 44e9ff5a..274219f0 100644 Binary files a/pg_data/base/16384/2659 and b/pg_data/base/16384/2659 differ diff --git a/pg_data/base/16384/2662 b/pg_data/base/16384/2662 index 8ad83e84..a61642ce 100644 Binary files a/pg_data/base/16384/2662 and b/pg_data/base/16384/2662 differ diff --git a/pg_data/base/16384/2663 b/pg_data/base/16384/2663 index 351c46e8..2cfdab42 100644 Binary files a/pg_data/base/16384/2663 and b/pg_data/base/16384/2663 differ diff --git a/pg_data/base/16384/2664 b/pg_data/base/16384/2664 index 708cbf01..86d19ef4 100644 Binary files a/pg_data/base/16384/2664 and b/pg_data/base/16384/2664 differ diff --git a/pg_data/base/16384/2665 b/pg_data/base/16384/2665 index 83ac8218..32671e38 100644 Binary files a/pg_data/base/16384/2665 and b/pg_data/base/16384/2665 differ diff --git a/pg_data/base/16384/2666 b/pg_data/base/16384/2666 index 7bea4825..49c8af2e 100644 Binary files a/pg_data/base/16384/2666 and b/pg_data/base/16384/2666 differ diff --git a/pg_data/base/16384/2667 b/pg_data/base/16384/2667 index 50228a1f..2f2ac904 100644 Binary files a/pg_data/base/16384/2667 and b/pg_data/base/16384/2667 differ diff --git a/pg_data/base/16384/2673 b/pg_data/base/16384/2673 index 4725b928..8a99c40d 100644 Binary files a/pg_data/base/16384/2673 and b/pg_data/base/16384/2673 differ diff --git a/pg_data/base/16384/2674 b/pg_data/base/16384/2674 index 02fca434..26329de2 100644 Binary files a/pg_data/base/16384/2674 and b/pg_data/base/16384/2674 differ diff --git a/pg_data/base/16384/2678 b/pg_data/base/16384/2678 index bd0ae35c..6df278b6 100644 Binary files a/pg_data/base/16384/2678 and b/pg_data/base/16384/2678 differ diff --git a/pg_data/base/16384/2679 b/pg_data/base/16384/2679 index 2420ee52..25b159e1 100644 Binary files a/pg_data/base/16384/2679 and b/pg_data/base/16384/2679 differ diff --git a/pg_data/base/16384/2696 b/pg_data/base/16384/2696 index 80e8830a..d5ec8ea9 100644 Binary files a/pg_data/base/16384/2696 and b/pg_data/base/16384/2696 differ diff --git a/pg_data/base/16384/2699 b/pg_data/base/16384/2699 index 42acad65..404da348 100644 Binary files a/pg_data/base/16384/2699 and b/pg_data/base/16384/2699 differ diff --git a/pg_data/base/16384/2701 b/pg_data/base/16384/2701 index d7c6a10b..3ff383e1 100644 Binary files a/pg_data/base/16384/2701 and b/pg_data/base/16384/2701 differ diff --git a/pg_data/base/16384/2702 b/pg_data/base/16384/2702 index 5114e8ab..4335b8a9 100644 Binary files a/pg_data/base/16384/2702 and b/pg_data/base/16384/2702 differ diff --git a/pg_data/base/16384/2703 b/pg_data/base/16384/2703 index a93daba0..35886a83 100644 Binary files a/pg_data/base/16384/2703 and b/pg_data/base/16384/2703 differ diff --git a/pg_data/base/16384/2704 b/pg_data/base/16384/2704 index 46146488..7bf3ab6f 100644 Binary files a/pg_data/base/16384/2704 and b/pg_data/base/16384/2704 differ diff --git a/pg_data/base/16384/3455 b/pg_data/base/16384/3455 index aba8e6d9..abf293a9 100644 Binary files a/pg_data/base/16384/3455 and b/pg_data/base/16384/3455 differ diff --git a/pg_data/base/16384/pg_internal.init b/pg_data/base/16384/pg_internal.init index b533d5fa..f56e51c7 100644 Binary files a/pg_data/base/16384/pg_internal.init and b/pg_data/base/16384/pg_internal.init differ diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index 90f7d7e9..f7b477f1 100644 Binary files a/pg_data/global/pg_control and b/pg_data/global/pg_control differ diff --git a/pg_data/pg_wal/000000010000000000000001 b/pg_data/pg_wal/000000010000000000000001 index a07d6f56..7410465f 100644 Binary files a/pg_data/pg_wal/000000010000000000000001 and b/pg_data/pg_wal/000000010000000000000001 differ diff --git a/pg_data/pg_xact/0000 b/pg_data/pg_xact/0000 index b23ff82b..2a37dfb4 100644 Binary files a/pg_data/pg_xact/0000 and b/pg_data/pg_xact/0000 differ