@startuml db-orm
' Schema source: Flyway V1–V72 (excl. V37, V43 — intentionally removed)
' Schema as of:  V72 (2026-06-08)
' ⚠ This is a versioned snapshot. Update when the schema changes significantly.

hide circle
skinparam linetype ortho

' ── Auth ──
package "Auth" {

  entity app_users {
    id : UUID <<PK>>
    --
    email : VARCHAR(255) NOT NULL UNIQUE
    password : VARCHAR(255) NOT NULL
    first_name : VARCHAR(100)
    last_name : VARCHAR(100)
    birth_date : DATE
    contact : TEXT
    enabled : BOOLEAN NOT NULL
    color : VARCHAR(20) NOT NULL
    notify_on_reply : BOOLEAN NOT NULL
    notify_on_mention : BOOLEAN NOT NULL
    created_at : TIMESTAMP
  }

  entity user_groups {
    id : UUID <<PK>>
    --
    name : VARCHAR(255) NOT NULL UNIQUE
  }

  entity app_users_groups {
    app_user_id : UUID <<FK>>
    group_id : UUID <<FK>>
  }

  entity group_permissions {
    group_id : UUID <<FK>>
    --
    permission : VARCHAR(255)
  }

  entity password_reset_tokens {
    id : UUID <<PK>>
    --
    app_user_id : UUID <<FK>>
    token : VARCHAR(64) NOT NULL UNIQUE
    expires_at : TIMESTAMP NOT NULL
    used : BOOLEAN NOT NULL
    created_at : TIMESTAMP NOT NULL
  }

  entity invite_tokens {
    id : UUID <<PK>>
    --
    code : VARCHAR(10) NOT NULL UNIQUE
    label : VARCHAR(255)
    max_uses : INTEGER
    use_count : INTEGER NOT NULL
    prefill_first_name : VARCHAR(255)
    prefill_last_name : VARCHAR(255)
    prefill_email : VARCHAR(255)
    expires_at : TIMESTAMP
    created_by : UUID <<FK>>
    created_at : TIMESTAMP NOT NULL
    revoked : BOOLEAN NOT NULL
  }

  entity invite_token_group_ids {
    invite_token_id : UUID <<FK>>
    group_id : UUID <<FK>>
  }
}

' ── Documents ──
package "Documents" {

  entity documents {
    id : UUID <<PK>>
    --
    title : VARCHAR(255) NOT NULL
    original_filename : VARCHAR(255) NOT NULL
    status : VARCHAR(255) NOT NULL
    file_path : VARCHAR(255)
    file_hash : VARCHAR(64)
    summary : TEXT
    transcription : TEXT
    meta_date : DATE
    meta_date_precision : VARCHAR(16) NOT NULL
    meta_date_end : DATE
    meta_date_raw : TEXT
    sender_text : TEXT
    receiver_text : TEXT
    meta_location : VARCHAR(255)
    meta_document_location : VARCHAR(255)
    archive_box : VARCHAR(255)
    archive_folder : VARCHAR(255)
    sender_id : UUID <<FK>>
    metadata_complete : BOOLEAN NOT NULL
    script_type : VARCHAR(30) NOT NULL
    thumbnail_key : VARCHAR(255)
    thumbnail_generated_at : TIMESTAMP
    thumbnail_aspect : VARCHAR(16)
    page_count : INTEGER
    search_vector : tsvector <<computed>>
    created_at : TIMESTAMP
    updated_at : TIMESTAMP
  }

  entity document_receivers {
    document_id : UUID <<FK>>
    person_id : UUID <<FK>>
  }

  entity document_tags {
    document_id : UUID <<FK>>
    tag_id : UUID <<FK>>
  }

  entity document_versions {
    id : UUID <<PK>>
    --
    document_id : UUID <<FK>>
    editor_id : UUID <<FK>>
    editor_name : VARCHAR(200) NOT NULL
    saved_at : TIMESTAMP NOT NULL
    snapshot : JSONB NOT NULL
    changed_fields : JSONB NOT NULL
  }

  entity document_annotations {
    id : UUID <<PK>>
    --
    document_id : UUID <<FK>>
    page_number : INTEGER NOT NULL
    x : DOUBLE PRECISION NOT NULL
    y : DOUBLE PRECISION NOT NULL
    width : DOUBLE PRECISION NOT NULL
    height : DOUBLE PRECISION NOT NULL
    color : VARCHAR(20) NOT NULL
    polygon : JSONB
    file_hash : VARCHAR(64)
    created_by : UUID <<FK>>
    created_at : TIMESTAMP NOT NULL
  }

  entity document_comments {
    id : UUID <<PK>>
    --
    document_id : UUID <<FK>>
    annotation_id : UUID <<FK>>
    block_id : UUID <<FK>>
    parent_id : UUID <<FK>>
    author_id : UUID <<FK>>
    author_name : VARCHAR(200) NOT NULL
    content : TEXT NOT NULL
    created_at : TIMESTAMP NOT NULL
    updated_at : TIMESTAMP NOT NULL
  }

  entity document_training_labels {
    document_id : UUID <<FK>>
    --
    label : VARCHAR(50) NOT NULL
  }

  entity comment_mentions {
    comment_id : UUID <<FK>>
    app_user_id : UUID <<FK>>
  }
}

' ── Persons ──
package "Persons" {

  entity persons {
    id : UUID <<PK>>
    --
    first_name : VARCHAR(255)
    last_name : VARCHAR(255) NOT NULL
    alias : VARCHAR(255)
    title : VARCHAR(50)
    person_type : VARCHAR(20) NOT NULL
    notes : TEXT
    birth_year : INTEGER
    death_year : INTEGER
    generation : SMALLINT
    family_member : BOOLEAN NOT NULL
    source_ref : VARCHAR(255) UNIQUE
    provisional : BOOLEAN NOT NULL
  }

  entity person_name_aliases {
    id : UUID <<PK>>
    --
    person_id : UUID <<FK>>
    last_name : VARCHAR(255) NOT NULL
    first_name : VARCHAR(255)
    type : VARCHAR(50) NOT NULL
    sort_order : INTEGER NOT NULL
    created_at : TIMESTAMPTZ
  }

  entity person_relationships {
    id : UUID <<PK>>
    --
    person_id : UUID <<FK>>
    related_person_id : UUID <<FK>>
    relation_type : VARCHAR(30) NOT NULL
    from_year : INTEGER
    to_year : INTEGER
    notes : VARCHAR(2000)
    created_at : TIMESTAMPTZ NOT NULL
  }
}

' ── Tags ──
package "Tags" {

  entity tag {
    id : UUID <<PK>>
    --
    name : VARCHAR(255) NOT NULL UNIQUE
    parent_id : UUID <<FK>>
    color : VARCHAR(20)
    source_ref : VARCHAR(255) UNIQUE
  }
}

' ── Transcription ──
package "Transcription" {

  entity transcription_blocks {
    id : UUID <<PK>>
    --
    annotation_id : UUID <<FK>>
    document_id : UUID <<FK>>
    text : TEXT
    label : VARCHAR(200)
    sort_order : INTEGER NOT NULL
    version : INTEGER NOT NULL
    source : VARCHAR(10) NOT NULL
    reviewed : BOOLEAN NOT NULL
    created_by : UUID <<FK>>
    updated_by : UUID <<FK>>
    created_at : TIMESTAMP NOT NULL
    updated_at : TIMESTAMP NOT NULL
  }

  entity transcription_block_versions {
    id : UUID <<PK>>
    --
    block_id : UUID <<FK>>
    text : TEXT NOT NULL
    changed_by : UUID <<FK>>
    changed_at : TIMESTAMP NOT NULL
  }

  entity transcription_block_mentioned_persons {
    block_id : UUID <<FK>>
    person_id : UUID NOT NULL <<FK>>
    --
    display_name : VARCHAR(200) NOT NULL
  }
}

' ── OCR ──
package "OCR" {

  entity ocr_jobs {
    id : UUID <<PK>>
    --
    status : VARCHAR(20) NOT NULL
    total_documents : INT NOT NULL
    processed_documents : INT NOT NULL
    error_count : INT NOT NULL
    skipped_count : INT NOT NULL
    created_by : UUID
    progress_message : TEXT
    created_at : TIMESTAMPTZ NOT NULL
    updated_at : TIMESTAMPTZ NOT NULL
  }

  entity ocr_job_documents {
    id : UUID <<PK>>
    --
    job_id : UUID <<FK>>
    document_id : UUID <<FK>>
    status : VARCHAR(20) NOT NULL
    error_message : TEXT
    current_page : INT
    total_pages : INT
    created_at : TIMESTAMPTZ NOT NULL
    updated_at : TIMESTAMPTZ NOT NULL
  }

  entity ocr_training_runs {
    id : UUID <<PK>>
    --
    status : VARCHAR(20) NOT NULL
    block_count : INT NOT NULL
    document_count : INT NOT NULL
    model_name : VARCHAR(100) NOT NULL
    error_message : TEXT
    triggered_by : UUID <<FK>>
    person_id : UUID <<FK>>
    cer : DOUBLE PRECISION
    loss : DOUBLE PRECISION
    accuracy : DOUBLE PRECISION
    epochs : INT
    created_at : TIMESTAMPTZ NOT NULL
    completed_at : TIMESTAMPTZ
  }

  entity sender_models {
    id : UUID <<PK>>
    --
    person_id : UUID <<FK>> UNIQUE
    model_path : TEXT NOT NULL
    accuracy : DOUBLE PRECISION
    cer : DOUBLE PRECISION
    corrected_lines_at_training : INT NOT NULL
    created_at : TIMESTAMPTZ NOT NULL
    updated_at : TIMESTAMPTZ NOT NULL
  }
}

' ── Supporting ──
package "Supporting" {

  entity notifications {
    id : UUID <<PK>>
    --
    recipient_id : UUID <<FK>>
    type : VARCHAR(32) NOT NULL
    document_id : UUID
    reference_id : UUID
    annotation_id : UUID
    actor_name : VARCHAR(255)
    read : BOOLEAN NOT NULL
    created_at : TIMESTAMP NOT NULL
  }

  entity audit_log {
    id : UUID <<PK>>
    --
    happened_at : TIMESTAMPTZ NOT NULL
    actor_id : UUID <<FK>>
    kind : VARCHAR(50) NOT NULL
    document_id : UUID <<FK>>
    payload : JSONB
  }

  entity geschichten {
    id : UUID <<PK>>
    --
    title : VARCHAR(255) NOT NULL
    body : TEXT
    status : VARCHAR(32) NOT NULL
    type : VARCHAR(32) NOT NULL
    author_id : UUID <<FK>>
    created_at : TIMESTAMP NOT NULL
    updated_at : TIMESTAMP NOT NULL
    published_at : TIMESTAMP
  }

  entity geschichten_persons {
    geschichte_id : UUID <<FK>>
    person_id : UUID <<FK>>
  }

  entity journey_items {
    id : UUID <<PK>>
    --
    geschichte_id : UUID <<FK>>
    document_id : UUID <<FK>>
    position : INTEGER NOT NULL CHECK (position > 0)
    note : TEXT CHECK (length <= 2000)
    ==
    UNIQUE (geschichte_id, position) DEFERRABLE INITIALLY DEFERRED
    UNIQUE (geschichte_id, document_id) WHERE document_id IS NOT NULL
  }
}

' Auth relationships
app_users_groups }o--|| app_users : app_user_id
app_users_groups }o--|| user_groups : group_id
group_permissions }o--|| user_groups : group_id
password_reset_tokens }o--|| app_users : app_user_id
invite_tokens }o--|| app_users : created_by
invite_token_group_ids }o--|| invite_tokens : invite_token_id
invite_token_group_ids }o--|| user_groups : group_id

' Document relationships
documents }o--o| persons : sender_id (ON DELETE SET NULL)
document_receivers }o--|| documents : document_id
document_receivers }o--|| persons : person_id (ON DELETE CASCADE)
document_tags }o--|| documents : document_id
document_tags }o--|| tag : tag_id
document_versions }o--|| documents : document_id
document_versions }o--o| app_users : editor_id
document_annotations }o--|| documents : document_id
document_annotations }o--o| app_users : created_by
document_comments }o--|| documents : document_id
document_comments }o--o| document_annotations : annotation_id
document_comments }o--o| transcription_blocks : block_id
document_comments }o--o| app_users : author_id
document_comments }o--o| document_comments : parent_id
document_training_labels }o--|| documents : document_id
comment_mentions }o--|| document_comments : comment_id
comment_mentions }o--|| app_users : app_user_id

' Person relationships
person_name_aliases }o--|| persons : person_id
person_relationships }o--|| persons : person_id
person_relationships }o--|| persons : related_person_id

' Tag self-reference
tag }o--o| tag : parent_id

' Transcription relationships
transcription_blocks }o--|| document_annotations : annotation_id
transcription_blocks }o--|| documents : document_id
transcription_blocks }o--o| app_users : created_by
transcription_blocks }o--o| app_users : updated_by
transcription_block_versions }o--|| transcription_blocks : block_id
transcription_block_versions }o--o| app_users : changed_by
transcription_block_mentioned_persons }o--|| transcription_blocks : block_id
transcription_block_mentioned_persons }o--|| persons : person_id (ON DELETE CASCADE)

' OCR relationships
ocr_job_documents }o--|| ocr_jobs : job_id
ocr_job_documents }o--|| documents : document_id
ocr_training_runs }o--o| app_users : triggered_by
ocr_training_runs }o--o| persons : person_id
sender_models ||--|| persons : person_id

' Supporting relationships
notifications }o--|| app_users : recipient_id
audit_log }o--o| app_users : actor_id
audit_log }o--o| documents : document_id
geschichten }o--o| app_users : author_id
geschichten_persons }o--|| geschichten : geschichte_id
geschichten_persons }o--|| persons : person_id
journey_items }o--|| geschichten : geschichte_id (ON DELETE CASCADE)
journey_items }o--o| documents : document_id (ON DELETE SET NULL)

@enduml
