diff --git a/backend/app/api/v1/faq_categories.py b/backend/app/api/v1/faq_categories.py new file mode 100644 index 00000000..b6ee1681 --- /dev/null +++ b/backend/app/api/v1/faq_categories.py @@ -0,0 +1,104 @@ +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 +from app.crud import audit as audit_crud +from app.crud import faq_category as category_crud +from app.crud import member as member_crud +from app.schemas.faq import CategoryCreate, CategoryRead, CategoryUpdate + +router = APIRouter() + + +def _check_permission_for_scope(study_id: uuid.UUID | None, current_user, member_role: str | None): + if study_id is None: + if current_user.role != "ADMIN": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ") + else: + if current_user.role != "ADMIN" and member_role != "PM": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + + +@router.post( + "/", + response_model=CategoryRead, + status_code=status.HTTP_201_CREATED, +) +async def create_category( + payload: CategoryCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> CategoryRead: + member_role = None + if payload.study_id: + member = await member_crud.get_member(db, payload.study_id, current_user.id) + if not member: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + member_role = member.role_in_study + _check_permission_for_scope(payload.study_id, current_user, member_role) + category = await category_crud.create_category(db, payload) + await audit_crud.log_action( + db, + study_id=payload.study_id, + entity_type="faq_category", + entity_id=category.id, + action="CREATE_FAQ_CATEGORY", + detail=f"FAQ category {category.name} created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return CategoryRead.model_validate(category) + + +@router.get( + "/", + response_model=list[CategoryRead], +) +async def list_categories( + study_id: uuid.UUID | None = None, + include_global: bool = True, + is_active: bool | None = True, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> list[CategoryRead]: + if study_id: + member = await member_crud.get_member(db, study_id, current_user.id) + if not member and current_user.role != "ADMIN": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + categories = await category_crud.list_categories(db, study_id, include_global=include_global, is_active=is_active) + return [CategoryRead.model_validate(c) for c in categories] + + +@router.patch( + "/{category_id}", + response_model=CategoryRead, +) +async def update_category( + category_id: uuid.UUID, + payload: CategoryUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> CategoryRead: + category = await category_crud.get_category(db, category_id) + if not category: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found") + member_role = None + if category.study_id: + from app.crud import member as member_crud + member = await member_crud.get_member(db, category.study_id, current_user.id) + member_role = member.role_in_study if member else None + _check_permission_for_scope(category.study_id, current_user, member_role) + updated = await category_crud.update_category(db, category, payload) + await audit_crud.log_action( + db, + study_id=category.study_id, + entity_type="faq_category", + entity_id=category_id, + action="UPDATE_FAQ_CATEGORY", + detail=f"FAQ category {category_id} updated", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return CategoryRead.model_validate(updated) diff --git a/backend/app/api/v1/faqs.py b/backend/app/api/v1/faqs.py new file mode 100644 index 00000000..517f1220 --- /dev/null +++ b/backend/app/api/v1/faqs.py @@ -0,0 +1,182 @@ +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 +from app.crud import audit as audit_crud +from app.crud import faq_category as category_crud +from app.crud import faq_item as faq_crud +from app.crud import member as member_crud +from app.schemas.faq import FaqCreate, FaqRead, FaqUpdate + +router = APIRouter() + + +def _check_write_permission(study_id: uuid.UUID | None, current_user, member_role: str | None): + if study_id is None: + if current_user.role != "ADMIN": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Only admin can manage global FAQ") + else: + if current_user.role != "ADMIN" and member_role != "PM": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + + +@router.post( + "/", + response_model=FaqRead, + status_code=status.HTTP_201_CREATED, +) +async def create_faq( + payload: FaqCreate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> FaqRead: + cat = await category_crud.get_category(db, payload.category_id) + if not cat: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Category not found") + if cat.study_id != payload.study_id: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Category scope mismatch") + member_role = None + if payload.study_id: + member = await member_crud.get_member(db, payload.study_id, current_user.id) + member_role = member.role_in_study if member else None + _check_write_permission(payload.study_id, current_user, member_role) + try: + item = await faq_crud.create_item(db, payload, created_by=current_user.id) + except ValueError as exc: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc + await audit_crud.log_action( + db, + study_id=payload.study_id, + entity_type="faq_item", + entity_id=item.id, + action="CREATE_FAQ_ITEM", + detail="FAQ created", + operator_id=current_user.id, + operator_role=current_user.role, + ) + return FaqRead.model_validate(item) + + +@router.get( + "/", + response_model=list[FaqRead], +) +async def list_faqs( + study_id: uuid.UUID | None = None, + category_id: uuid.UUID | None = None, + keyword: str | None = None, + is_active: bool | None = True, + study_scope: str | None = "project", + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> list[FaqRead]: + membership_cache: dict[uuid.UUID, str | None] = {} + + async def _get_member_role(sid: uuid.UUID) -> str | None: + if sid in membership_cache: + return membership_cache[sid] + member = await member_crud.get_member(db, sid, current_user.id) + role = member.role_in_study if member else None + membership_cache[sid] = role + return role + + if study_id: + if current_user.role != "ADMIN": + role = await _get_member_role(study_id) + if not role: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + + if is_active is False and current_user.role != "ADMIN": + role = None + if study_id: + role = await _get_member_role(study_id) + if role != "PM": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions") + + items = await faq_crud.list_items( + db, + study_id=study_id, + category_id=category_id, + keyword=keyword, + is_active=is_active, + study_scope=study_scope, + ) + + visible: list[FaqRead] = [] + for it in items: + if it.study_id and current_user.role != "ADMIN": + role = await _get_member_role(it.study_id) + if not role: + continue + if not it.is_active and current_user.role != "ADMIN": + role = await _get_member_role(it.study_id) if it.study_id else None + if role != "PM": + continue + visible.append(FaqRead.model_validate(it)) + return visible + + +@router.get( + "/{item_id}", + response_model=FaqRead, +) +async def get_faq( + item_id: uuid.UUID, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> FaqRead: + item = await faq_crud.get_item(db, item_id) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + if item.study_id and current_user.role != "ADMIN": + member = await member_crud.get_member(db, item.study_id, current_user.id) + if not member: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not a study member") + if not item.is_active and current_user.role not in {"ADMIN"}: + member_role = None + if item.study_id: + member = await member_crud.get_member(db, item.study_id, current_user.id) + member_role = member.role_in_study if member else None + if member_role != "PM": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive FAQ") + return FaqRead.model_validate(item) + + +@router.patch( + "/{item_id}", + response_model=FaqRead, +) +async def update_faq( + item_id: uuid.UUID, + payload: FaqUpdate, + db: AsyncSession = Depends(get_db_session), + current_user=Depends(get_current_user), +) -> FaqRead: + item = await faq_crud.get_item(db, item_id) + if not item: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="FAQ not found") + member_role = None + if item.study_id: + member = await member_crud.get_member(db, item.study_id, current_user.id) + member_role = member.role_in_study if member else None + _check_write_permission(item.study_id, current_user, member_role) + old_active = item.is_active + updated = await faq_crud.update_item(db, item, payload) + action = "UPDATE_FAQ_ITEM" + detail = "FAQ updated" + if payload.is_active is not None and payload.is_active != old_active: + action = "FAQ_STATUS_CHANGE" + detail = "FAQ disabled" if not payload.is_active else "FAQ enabled" + await audit_crud.log_action( + db, + study_id=item.study_id, + entity_type="faq_item", + entity_id=item_id, + action=action, + detail=detail, + operator_id=current_user.id, + operator_role=current_user.role, + ) + return FaqRead.model_validate(updated) diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 4a068472..97a7dc44 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, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard +from app.api.v1 import auth, users, studies, sites, members, comments, attachments, audit_logs, milestones, tasks, dashboard, subjects, visits, aes, issues, data_queries, verifications, imp_products, imp_batches, imp_inventory, imp_transactions, finance, finance_dashboard, faq_categories, faqs api_router = APIRouter() api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) @@ -26,3 +26,5 @@ api_router.include_router(imp_inventory.router, prefix="/studies/{study_id}/imp/ api_router.include_router(imp_transactions.router, prefix="/studies/{study_id}/imp/transactions", tags=["imp-transactions"]) api_router.include_router(finance.router, prefix="/studies/{study_id}/finance", tags=["finance"]) api_router.include_router(finance_dashboard.router, prefix="/studies/{study_id}/finance", tags=["finance"]) +api_router.include_router(faq_categories.router, prefix="/faqs/categories", tags=["faq-categories"]) +api_router.include_router(faqs.router, prefix="/faqs/items", tags=["faqs"]) diff --git a/backend/app/crud/faq_category.py b/backend/app/crud/faq_category.py new file mode 100644 index 00000000..7390e70f --- /dev/null +++ b/backend/app/crud/faq_category.py @@ -0,0 +1,58 @@ +import uuid +from typing import Sequence + +from sqlalchemy import select, update as sa_update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.faq_category import FaqCategory +from app.schemas.faq import CategoryCreate, CategoryUpdate + + +async def create_category(db: AsyncSession, category_in: CategoryCreate) -> FaqCategory: + category = FaqCategory( + study_id=category_in.study_id, + name=category_in.name, + description=category_in.description, + sort_order=category_in.sort_order, + is_active=category_in.is_active, + ) + db.add(category) + await db.commit() + await db.refresh(category) + return category + + +async def list_categories( + db: AsyncSession, + study_id: uuid.UUID | None, + include_global: bool = True, + is_active: bool | None = True, +) -> Sequence[FaqCategory]: + stmt = select(FaqCategory) + if include_global: + stmt = stmt.where((FaqCategory.study_id == study_id) | (FaqCategory.study_id.is_(None))) + else: + stmt = stmt.where(FaqCategory.study_id == study_id) + if is_active is not None: + stmt = stmt.where(FaqCategory.is_active == is_active) + stmt = stmt.order_by(FaqCategory.sort_order, FaqCategory.name) + result = await db.execute(stmt) + return result.scalars().all() + + +async def get_category(db: AsyncSession, category_id: uuid.UUID) -> FaqCategory | None: + result = await db.execute(select(FaqCategory).where(FaqCategory.id == category_id)) + return result.scalar_one_or_none() + + +async def update_category(db: AsyncSession, category: FaqCategory, category_in: CategoryUpdate) -> FaqCategory: + update_data = category_in.model_dump(exclude_unset=True) + if update_data: + await db.execute( + sa_update(FaqCategory) + .where(FaqCategory.id == category.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(category) + return category diff --git a/backend/app/crud/faq_item.py b/backend/app/crud/faq_item.py new file mode 100644 index 00000000..d49276d9 --- /dev/null +++ b/backend/app/crud/faq_item.py @@ -0,0 +1,90 @@ +import uuid +from typing import Sequence + +from sqlalchemy import or_, select, update as sa_update +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.faq_category import FaqCategory +from app.models.faq_item import FaqItem +from app.schemas.faq import FaqCreate, FaqUpdate + + +async def create_item(db: AsyncSession, item_in: FaqCreate, *, created_by: uuid.UUID) -> FaqItem: + # ensure category exists and matches study scope + category = await db.get(FaqCategory, item_in.category_id) + if not category: + raise ValueError("Category not found") + if category.study_id != item_in.study_id: + # allow global category if item is global (None) or project if matches + if not (category.study_id is None and item_in.study_id is None): + raise ValueError("Category scope mismatch") + + item = FaqItem( + study_id=item_in.study_id, + category_id=item_in.category_id, + question=item_in.question, + answer=item_in.answer, + keywords=item_in.keywords, + version=1, + is_active=True, + created_by=created_by, + ) + db.add(item) + await db.commit() + await db.refresh(item) + return item + + +async def get_item(db: AsyncSession, item_id: uuid.UUID) -> FaqItem | None: + result = await db.execute(select(FaqItem).where(FaqItem.id == item_id)) + return result.scalar_one_or_none() + + +async def list_items( + db: AsyncSession, + *, + study_id: uuid.UUID | None, + category_id: uuid.UUID | None = None, + keyword: str | None = None, + is_active: bool | None = True, + study_scope: str | None = "project", +) -> Sequence[FaqItem]: + stmt = select(FaqItem).join(FaqCategory, FaqCategory.id == FaqItem.category_id) + + if study_scope == "all": + pass + elif study_scope == "global": + stmt = stmt.where(FaqItem.study_id.is_(None)) + else: # project (default) + stmt = stmt.where((FaqItem.study_id == study_id) | (FaqItem.study_id.is_(None))) + + if category_id: + stmt = stmt.where(FaqItem.category_id == category_id) + if keyword: + like_pattern = f"%{keyword}%" + stmt = stmt.where( + or_( + FaqItem.question.ilike(like_pattern), + FaqItem.answer.ilike(like_pattern), + FaqItem.keywords.ilike(like_pattern), + ) + ) + if is_active is not None: + stmt = stmt.where(FaqItem.is_active == is_active) + result = await db.execute(stmt) + return result.scalars().all() + + +async def update_item(db: AsyncSession, item: FaqItem, item_in: FaqUpdate) -> FaqItem: + update_data = item_in.model_dump(exclude_unset=True) + if "question" in update_data or "answer" in update_data or "keywords" in update_data: + update_data["version"] = item.version + 1 + if update_data: + await db.execute( + sa_update(FaqItem) + .where(FaqItem.id == item.id) + .values(**update_data) + ) + await db.commit() + await db.refresh(item) + return item diff --git a/backend/app/db/base.py b/backend/app/db/base.py index d726c9e3..5b9ce9cb 100644 --- a/backend/app/db/base.py +++ b/backend/app/db/base.py @@ -21,3 +21,5 @@ from app.models.imp_batch import ImpBatch # noqa: F401 from app.models.imp_inventory import ImpInventory # noqa: F401 from app.models.imp_transaction import ImpTransaction # noqa: F401 from app.models.finance import FinanceItem # noqa: F401 +from app.models.faq_category import FaqCategory # noqa: F401 +from app.models.faq_item import FaqItem # noqa: F401 diff --git a/backend/app/models/faq_category.py b/backend/app/models/faq_category.py new file mode 100644 index 00000000..ec962948 --- /dev/null +++ b/backend/app/models/faq_category.py @@ -0,0 +1,24 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, UniqueConstraint, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class FaqCategory(Base): + __tablename__ = "faq_categories" + __table_args__ = (UniqueConstraint("study_id", "name", name="uq_faq_category_name"),) + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=True, index=True) + name: Mapped[str] = mapped_column(String(100), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + sort_order: Mapped[int] = mapped_column(nullable=False, default=0) + is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=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/faq_item.py b/backend/app/models/faq_item.py new file mode 100644 index 00000000..b9d1f42e --- /dev/null +++ b/backend/app/models/faq_item.py @@ -0,0 +1,26 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text, func +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import Mapped, mapped_column + +from app.db.base_class import Base + + +class FaqItem(Base): + __tablename__ = "faq_items" + + id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + study_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("studies.id"), nullable=True, index=True) + category_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("faq_categories.id"), index=True, nullable=False) + question: Mapped[str] = mapped_column(Text, nullable=False) + answer: Mapped[str] = mapped_column(Text, nullable=False) + keywords: Mapped[str | None] = mapped_column(String(255), nullable=True) + version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=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/faq.py b/backend/app/schemas/faq.py new file mode 100644 index 00000000..6b284695 --- /dev/null +++ b/backend/app/schemas/faq.py @@ -0,0 +1,64 @@ +import uuid +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict + + +class CategoryCreate(BaseModel): + study_id: Optional[uuid.UUID] = None + name: str + description: Optional[str] = None + sort_order: int = 0 + is_active: bool = True + + +class CategoryUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + sort_order: Optional[int] = None + is_active: Optional[bool] = None + + +class CategoryRead(BaseModel): + id: uuid.UUID + study_id: Optional[uuid.UUID] + name: str + description: Optional[str] + sort_order: int + is_active: bool + created_at: datetime + updated_at: datetime + + model_config = ConfigDict(from_attributes=True) + + +class FaqCreate(BaseModel): + study_id: Optional[uuid.UUID] = None + category_id: uuid.UUID + question: str + answer: str + keywords: Optional[str] = None + + +class FaqUpdate(BaseModel): + question: Optional[str] = None + answer: Optional[str] = None + keywords: Optional[str] = None + is_active: Optional[bool] = None + + +class FaqRead(BaseModel): + id: uuid.UUID + study_id: Optional[uuid.UUID] + category_id: uuid.UUID + question: str + answer: str + keywords: Optional[str] + version: int + is_active: bool + 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 03075762..78a49762 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 ace6776a..7ddcdfec 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 c6517417..e868f871 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 397b8188..4a9bf26a 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 49445157..2c57ab74 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 4bc6a877..3b37e0e5 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 92af8e3b..56c548f2 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 6276eed1..55b3c926 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 948695e8..643613c7 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 c69c60e5..e5c01303 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 23fe263e..a2083859 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 06d0e933..ae039382 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 4ca47523..f0dfda5c 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 4bddca0c..8d4111dd 100644 Binary files a/pg_data/base/16384/24672 and b/pg_data/base/16384/24672 differ diff --git a/pg_data/base/16384/24672_fsm b/pg_data/base/16384/24672_fsm new file mode 100644 index 00000000..6b4fbb37 Binary files /dev/null and b/pg_data/base/16384/24672_fsm differ diff --git a/pg_data/base/16384/24678 b/pg_data/base/16384/24678 index bfda699a..f481008d 100644 Binary files a/pg_data/base/16384/24678 and b/pg_data/base/16384/24678 differ diff --git a/pg_data/base/16384/25066 b/pg_data/base/16384/25066 new file mode 100644 index 00000000..22b66781 Binary files /dev/null and b/pg_data/base/16384/25066 differ diff --git a/pg_data/base/16384/25071 b/pg_data/base/16384/25071 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/25072 b/pg_data/base/16384/25072 new file mode 100644 index 00000000..834d4ff5 Binary files /dev/null and b/pg_data/base/16384/25072 differ diff --git a/pg_data/base/16384/25073 b/pg_data/base/16384/25073 new file mode 100644 index 00000000..f3c45039 Binary files /dev/null and b/pg_data/base/16384/25073 differ diff --git a/pg_data/base/16384/25075 b/pg_data/base/16384/25075 new file mode 100644 index 00000000..47668139 Binary files /dev/null and b/pg_data/base/16384/25075 differ diff --git a/pg_data/base/16384/25082 b/pg_data/base/16384/25082 new file mode 100644 index 00000000..19111bb2 Binary files /dev/null and b/pg_data/base/16384/25082 differ diff --git a/pg_data/base/16384/25083 b/pg_data/base/16384/25083 new file mode 100644 index 00000000..6d17cf9d Binary files /dev/null and b/pg_data/base/16384/25083 differ diff --git a/pg_data/base/16384/25088 b/pg_data/base/16384/25088 new file mode 100644 index 00000000..e69de29b diff --git a/pg_data/base/16384/25089 b/pg_data/base/16384/25089 new file mode 100644 index 00000000..61676fa2 Binary files /dev/null and b/pg_data/base/16384/25089 differ diff --git a/pg_data/base/16384/25090 b/pg_data/base/16384/25090 new file mode 100644 index 00000000..30e79a8d Binary files /dev/null and b/pg_data/base/16384/25090 differ diff --git a/pg_data/base/16384/25107 b/pg_data/base/16384/25107 new file mode 100644 index 00000000..fbc0b577 Binary files /dev/null and b/pg_data/base/16384/25107 differ diff --git a/pg_data/base/16384/25108 b/pg_data/base/16384/25108 new file mode 100644 index 00000000..05753329 Binary files /dev/null and b/pg_data/base/16384/25108 differ diff --git a/pg_data/base/16384/2579 b/pg_data/base/16384/2579 index d17ef732..be328d53 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 515d39a6..2e358424 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 9e809110..785bc668 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 1b9d13e0..836fc945 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 b25a2783..c394668b 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 57fb33fe..cffb4074 100644 Binary files a/pg_data/base/16384/2610 and b/pg_data/base/16384/2610 differ diff --git a/pg_data/base/16384/2620 b/pg_data/base/16384/2620 index 5e6aef6f..94619c7c 100644 Binary files a/pg_data/base/16384/2620 and b/pg_data/base/16384/2620 differ diff --git a/pg_data/base/16384/2656 b/pg_data/base/16384/2656 index 9c2ebb58..88b4d1ab 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 cab6b01f..7b905e3d 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 13bdfc49..8408f5cf 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 eadd482f..3b304d06 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 da21f7cc..3c38c157 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 173a088c..5f738b3e 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 a8f23495..16fe4c25 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 6b042946..530fefd3 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 7a5cfbdc..a9570d81 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 60973054..42e18d20 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 3de00dc3..aa46d947 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 79a297a1..3a84b45e 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 1fe16301..d7a1a1fe 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 674a67a3..1fff3921 100644 Binary files a/pg_data/base/16384/2679 and b/pg_data/base/16384/2679 differ diff --git a/pg_data/base/16384/2699 b/pg_data/base/16384/2699 index e8f2446e..4d1cf4eb 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 a3bb06df..236d923f 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 d87b4f64..9396b244 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 6669d545..f684a3e6 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 a5634941..231dd291 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 86708d61..947b9758 100644 Binary files a/pg_data/base/16384/3455 and b/pg_data/base/16384/3455 differ diff --git a/pg_data/global/pg_control b/pg_data/global/pg_control index e66830af..e7e9fba4 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 294c5b5f..b3c74466 100644 Binary files a/pg_data/pg_wal/000000010000000000000001 and b/pg_data/pg_wal/000000010000000000000001 differ diff --git a/pg_data/pg_wal/000000010000000000000002 b/pg_data/pg_wal/000000010000000000000002 new file mode 100644 index 00000000..dba78e91 Binary files /dev/null and b/pg_data/pg_wal/000000010000000000000002 differ diff --git a/pg_data/pg_xact/0000 b/pg_data/pg_xact/0000 index 4afef390..0f749b6f 100644 Binary files a/pg_data/pg_xact/0000 and b/pg_data/pg_xact/0000 differ