"""Publishing, domain-edge refs, SEO & PWA shells — Phase 11.7. PublishRelease = publish workflow audit (site/page) beyond simple status flips. DomainEdgeBinding = DNS/SSL/edge map refs (ADR-009); edge ownership stays Core/Nginx. SeoMetadata / SitemapShell / RobotsShell = SEO document shells. PwaManifest = PWA readiness shell (manifest + service-worker ref). No bundles/feature toggles in this phase. """ from __future__ import annotations import uuid from datetime import datetime from sqlalchemy import DateTime, Index, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.types import JSON from app.core.database import Base from app.models.base import ( ActorAuditMixin, OptimisticLockMixin, SoftDeleteMixin, TenantMixin, TimestampMixin, UUIDPrimaryKeyMixin, ) from app.models.types import ( GUID, DomainEdgeStatus, PublishReleaseStatus, PublishTarget, PwaStatus, RobotsStatus, SeoStatus, SeoTarget, SitemapStatus, ) class PublishRelease( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Publish workflow shell for a site or page.""" __tablename__ = "experience_publish_releases" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) target_type: Mapped[PublishTarget] = mapped_column(nullable=False) target_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[PublishReleaseStatus] = mapped_column( default=PublishReleaseStatus.PENDING, nullable=False ) notes: Mapped[str | None] = mapped_column(Text, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) completed_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( Index( "ix_experience_publish_releases_target", "tenant_id", "target_type", "target_id", ), Index("ix_experience_publish_releases_status", "tenant_id", "status"), Index( "ix_experience_publish_releases_tenant_deleted", "tenant_id", "is_deleted" ), ) class DomainEdgeBinding( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Edge DNS/SSL binding refs for a site domain (ADR-009) — no Nginx ownership.""" __tablename__ = "experience_domain_edge_bindings" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_domain_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) host: Mapped[str] = mapped_column(String(255), nullable=False) status: Mapped[DomainEdgeStatus] = mapped_column( default=DomainEdgeStatus.PENDING, nullable=False ) dns_status_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) ssl_cert_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) edge_map_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_domain_id", name="uq_experience_domain_edge_bindings_domain", ), Index("ix_experience_domain_edge_bindings_site", "tenant_id", "site_id"), Index("ix_experience_domain_edge_bindings_host", "tenant_id", "host"), Index("ix_experience_domain_edge_bindings_status", "tenant_id", "status"), Index( "ix_experience_domain_edge_bindings_tenant_deleted", "tenant_id", "is_deleted", ), ) class SeoMetadata( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """SEO metadata shell for a site or page.""" __tablename__ = "experience_seo_metadata" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) target_type: Mapped[SeoTarget] = mapped_column(nullable=False) target_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[SeoStatus] = mapped_column(default=SeoStatus.DRAFT, nullable=False) title: Mapped[str | None] = mapped_column(String(255), nullable=True) description: Mapped[str | None] = mapped_column(Text, nullable=True) canonical_url: Mapped[str | None] = mapped_column(String(500), nullable=True) robots_directive: Mapped[str | None] = mapped_column(String(100), nullable=True) og_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "target_type", "target_id", name="uq_experience_seo_metadata_target", ), Index("ix_experience_seo_metadata_status", "tenant_id", "status"), Index( "ix_experience_seo_metadata_tenant_deleted", "tenant_id", "is_deleted" ), ) class SitemapShell( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Sitemap document shell for a site.""" __tablename__ = "experience_sitemap_shells" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[SitemapStatus] = mapped_column( default=SitemapStatus.DRAFT, nullable=False ) entries_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) storage_file_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_id", name="uq_experience_sitemap_shells_site" ), Index("ix_experience_sitemap_shells_status", "tenant_id", "status"), Index( "ix_experience_sitemap_shells_tenant_deleted", "tenant_id", "is_deleted" ), ) class RobotsShell( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """Robots.txt shell for a site.""" __tablename__ = "experience_robots_shells" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[RobotsStatus] = mapped_column( default=RobotsStatus.DRAFT, nullable=False ) content_text: Mapped[str | None] = mapped_column(Text, nullable=True) rules_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_id", name="uq_experience_robots_shells_site" ), Index("ix_experience_robots_shells_status", "tenant_id", "status"), Index( "ix_experience_robots_shells_tenant_deleted", "tenant_id", "is_deleted" ), ) class PwaManifest( Base, UUIDPrimaryKeyMixin, TenantMixin, TimestampMixin, SoftDeleteMixin, ActorAuditMixin, OptimisticLockMixin, ): """PWA readiness shell — manifest JSON + service-worker Storage ref.""" __tablename__ = "experience_pwa_manifests" workspace_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) site_id: Mapped[uuid.UUID] = mapped_column(GUID(), nullable=False) status: Mapped[PwaStatus] = mapped_column(default=PwaStatus.DRAFT, nullable=False) name: Mapped[str | None] = mapped_column(String(255), nullable=True) short_name: Mapped[str | None] = mapped_column(String(100), nullable=True) start_url: Mapped[str | None] = mapped_column(String(500), nullable=True) manifest_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) service_worker_ref: Mapped[str | None] = mapped_column(String(255), nullable=True) metadata_json: Mapped[dict | None] = mapped_column(JSON, nullable=True) __table_args__ = ( UniqueConstraint( "tenant_id", "site_id", name="uq_experience_pwa_manifests_site" ), Index("ix_experience_pwa_manifests_status", "tenant_id", "status"), Index( "ix_experience_pwa_manifests_tenant_deleted", "tenant_id", "is_deleted" ), )