V5.1 UI refinements
Quality Gate / gate (push) Failing after 11s

This commit is contained in:
Jim Lancaster
2026-08-23 15:28:41 -05:00
parent ae3483ec2e
commit ebf659b26c
12 changed files with 58 additions and 33 deletions
+2 -1
View File
@@ -50,6 +50,7 @@ Optional:
Rules: Rules:
- Exact date must parse as `YYYY-MM-DD`; browser presentation may follow locale. - Exact date must parse as `YYYY-MM-DD`; browser presentation may follow locale.
- The exact-date input is labeled **Document date**.
- Existing people appear with disambiguating labels. - Existing people appear with disambiguating labels.
- Tag assignment supports selecting existing tags and adding new labels inline. - Tag assignment supports selecting existing tags and adding new labels inline.
- **Create new person** opens Person creation. - **Create new person** opens Person creation.
@@ -68,7 +69,7 @@ Rules:
- The heading shows name, type, and internal ID. - The heading shows name, type, and internal ID.
- The first Source, when present, appears in the dark-room viewer. - The first Source, when present, appears in the dark-room viewer.
- Archival Metadata shows authors, Document Type, tags, compact Document date, location, and archive identifier. Notes appear in a separate archival-notes block within the same card. - Archival Metadata shows authors, Document Type, tags, Document date (`MM-DD-YYYY` for exact dates), location, and archive identifier. Notes appear in a separate archival-notes block within the same card.
- System Logistics shows created and updated timestamps. - System Logistics shows created and updated timestamps.
- Related People are grouped by role and link to Person Detail. - Related People are grouped by role and link to Person Detail.
- **Sources & Pipeline Jobs** shows counts and actions for filtered Sources, Document Jobs, and adding a Job. - **Sources & Pipeline Jobs** shows counts and actions for filtered Sources, Document Jobs, and adding a Job.
+2 -2
View File
@@ -20,8 +20,8 @@ People manages reusable historical-person records. A Person may appear in many D
- The title is **Archival Entities: People**. - The title is **Archival Entities: People**.
- **Create new person** opens the create route. - **Create new person** opens the create route.
- The table defaults to Last Name order and supports search and column sorting. - The table defaults to Last Name order and supports search and column sorting.
- Columns are Last Name, First & Middle, FamilySearch ID, Birth Date, Death Date, and # Documents. - Columns are Last Name, First & Middle, Tags, FamilySearch ID, Birth Date, Death Date, and # Documents.
- Last Name and First & Middle are left-aligned; FamilySearch ID, date columns, and # Documents are centered. - Name and Tags are left-aligned; FamilySearch ID, date columns, and # Documents are centered.
- # Documents reflects how many linked Documents each Person is connected to. - # Documents reflects how many linked Documents each Person is connected to.
- Birth and death values independently prefer exact date, then approximate date, then `Unknown`. - Birth and death values independently prefer exact date, then approximate date, then `Unknown`.
- Selecting a row opens Person Detail. - Selecting a row opens Person Detail.
+2 -1
View File
@@ -22,6 +22,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document
- Rows are ordered by page number and then upload name. - Rows are ordered by page number and then upload name.
- Columns are Document Name, Page Number, Upload Title, Status, and Error Detail. - Columns are Document Name, Page Number, Upload Title, Status, and Error Detail.
- Document Name, Upload Title, and Error Detail are left-aligned; Status is centered. - Document Name, Upload Title, and Error Detail are left-aligned; Status is centered.
- Status labels are presented in uppercase for consistency with Jobs.
- Stored Filename is intentionally absent from the list. - Stored Filename is intentionally absent from the list.
- Selecting a row opens Source Detail. - Selecting a row opens Source Detail.
- No records displays `No source asset records found in repository.` - No records displays `No source asset records found in repository.`
@@ -39,7 +40,7 @@ The list accepts optional `document_id` and `job_id` query parameters. Document
- Wide+narrow landscape images switch to a stacked left layout (image above Editable Revision) with metadata on the right. - Wide+narrow landscape images switch to a stacked left layout (image above Editable Revision) with metadata on the right.
- Editable Revision is seeded from an existing revision or the preferred machine transcription. - Editable Revision is seeded from an existing revision or the preferred machine transcription.
- Source Metadata shows upload name, stored filename, page number, Document Name, Document ID, and stored path. Source ID appears in the page-header subtitle. - Source Metadata shows upload name, stored filename, page number, Document Name, Document ID, and stored path. Source ID appears in the page-header subtitle.
- SourceJob Metadata shows latest status, Job ID, execution time, provider, model, prompt, and failure detail. - SourceJob Metadata shows latest status (uppercase display), Job ID, execution time, provider, model, prompt, and failure detail.
- Revision Logistics shows revised state, last-revised time, and upload time. - Revision Logistics shows revised state, last-revised time, and upload time.
- Candidate Machine Transcriptions appears below the image/revision area, remains compact until expanded, then compares it with the preferred - Candidate Machine Transcriptions appears below the image/revision area, remains compact until expanded, then compares it with the preferred
machine result and requires confirmation before **Use this transcription**. machine result and requires confirmation before **Use this transcription**.
+14 -14
View File
@@ -19,8 +19,8 @@ class PersonTableRow:
"""Read model consumed by the people table component.""" """Read model consumed by the people table component."""
id: UUID id: UUID
last_name: str name: str
given_names: str tags: str
family_search_id: str family_search_id: str
birth_date: str birth_date: str
death_date: str death_date: str
@@ -31,8 +31,8 @@ def _serialize_rows(rows: Sequence[PersonTableRow]) -> list[dict[str, Any]]:
return [ return [
{ {
"id": str(row.id), "id": str(row.id),
"last_name": row.last_name, "name": row.name,
"given_names": row.given_names, "tags": row.tags or "Not set",
"family_search_id": row.family_search_id or "Not set", "family_search_id": row.family_search_id or "Not set",
"birth_date": row.birth_date or "Unknown", "birth_date": row.birth_date or "Unknown",
"death_date": row.death_date or "Unknown", "death_date": row.death_date or "Unknown",
@@ -53,18 +53,18 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None:
rows=_serialize_rows(rows), rows=_serialize_rows(rows),
columns=[ columns=[
{ {
"name": "last_name", "name": "name",
"label": "Last Name", "label": "Last Name, First & Middle",
"field": "last_name", "field": "name",
"sortable": True, "sortable": True,
"classes": "font-serif font-semibold text-left ui-table-cell-wrap", "classes": "font-serif font-semibold text-left ui-table-cell-wrap",
}, },
{ {
"name": "given_names", "name": "tags",
"label": "First & Middle", "label": "Tags",
"field": "given_names", "field": "tags",
"sortable": True, "sortable": True,
"classes": "font-serif font-semibold text-left ui-table-cell-wrap", "classes": "text-left ui-table-cell-wrap",
}, },
{"name": "family_search_id", "label": "FamilySearch ID", "field": "family_search_id", "sortable": True}, {"name": "family_search_id", "label": "FamilySearch ID", "field": "family_search_id", "sortable": True},
{ {
@@ -92,14 +92,14 @@ def render_people_table(rows: Sequence[PersonTableRow]) -> None:
"classes": "font-mono", "classes": "font-mono",
}, },
], ],
default_sort_by="last_name", default_sort_by="name",
search_placeholder="Search people by last name, given names, FamilySearch ID, or dates...", search_placeholder="Search people by name, tags, FamilySearch ID, or dates...",
on_row_click_id=lambda person_id: ui.navigate.to(f"/people/{person_id}"), on_row_click_id=lambda person_id: ui.navigate.to(f"/people/{person_id}"),
) )
# Custom column template adding an archival entity icon next to person's name # Custom column template adding an archival entity icon next to person's name
table.add_slot( table.add_slot(
"body-cell-last_name", "body-cell-name",
r""" r"""
<q-td :props="props"> <q-td :props="props">
<div class="row items-center q-gutter-x-xs"> <div class="row items-center q-gutter-x-xs">
@@ -35,7 +35,7 @@ def _serialize_rows(rows: Sequence[SourceTableRow]) -> list[dict[str, Any]]:
"upload_name": row.upload_name, "upload_name": row.upload_name,
"document_id": str(row.document_id), "document_id": str(row.document_id),
"document_name": row.document_name or "-", "document_name": row.document_name or "-",
"job_source_status": row.job_source_status or "-", "job_source_status": (row.job_source_status or "-").upper(),
"job_source_error_detail": row.job_source_error_detail or "-", "job_source_error_detail": row.job_source_error_detail or "-",
} }
for row in rows for row in rows
@@ -109,7 +109,7 @@ def render_sources_table(rows: Sequence[SourceTableRow]) -> None:
dense dense
square square
size="sm" size="sm"
:class="`ui-status ui-status--${props.value}`" :class="`ui-status ui-status--${props.value.toLowerCase()}`"
> >
{{ props.value }} {{ props.value }}
</q-chip> </q-chip>
+9 -2
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
@@ -448,7 +449,7 @@ def _render_document_form_fields(
with ui.row().classes("w-full gap-3 grid grid-cols-1 md:grid-cols-2"): with ui.row().classes("w-full gap-3 grid grid-cols-1 md:grid-cols-2"):
date_input = ( date_input = (
ui.input( ui.input(
label="Exact date (YYYY-MM-DD)", label="Document date",
value=document.document_date.isoformat() if document and document.document_date else "", value=document.document_date.isoformat() if document and document.document_date else "",
) )
.props('outlined type="date"') .props('outlined type="date"')
@@ -542,7 +543,7 @@ def _render_bento_metadata_zone(document: Document) -> None:
key=str.casefold, key=str.casefold,
) )
metadata_row("Tags:", ", ".join(tags) if tags else "Not set") metadata_row("Tags:", ", ".join(tags) if tags else "Not set")
metadata_row("Document Date:", compact_date(document.document_date, document.document_date_raw)) metadata_row("Document Date:", _detail_document_date(document.document_date, document.document_date_raw))
metadata_row("Location Created:", document.location_created or "Not set") metadata_row("Location Created:", document.location_created or "Not set")
metadata_row("Archive Identifier:", document.archive_identifier or "Not set") metadata_row("Archive Identifier:", document.archive_identifier or "Not set")
@@ -555,6 +556,12 @@ def _render_bento_metadata_zone(document: Document) -> None:
ui.label(f"Updated: {document.updated_at.isoformat()}").classes("text-[11px] ui-text-muted") ui.label(f"Updated: {document.updated_at.isoformat()}").classes("text-[11px] ui-text-muted")
def _detail_document_date(exact: date | None, approximate: str | None) -> str:
if exact is not None:
return exact.strftime("%m-%d-%Y")
return (approximate or "").strip() or "Unknown"
def _render_bento_relations_zone(document: Document) -> None: def _render_bento_relations_zone(document: Document) -> None:
with ui.column().classes("col-span-12 lg:col-span-4 gap-4"): with ui.column().classes("col-span-12 lg:col-span-4 gap-4"):
_render_related_people_card(document) _render_related_people_card(document)
+2 -2
View File
@@ -313,7 +313,7 @@ def register_page() -> None: # noqa: PLR0915
with archival_card(extra_classes="gap-2"): with archival_card(extra_classes="gap-2"):
ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary") ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary")
metadata_row("Current Status:", job.status.value) metadata_row("Current Status:", job.status.value.upper())
ui.label( ui.label(
"Cancel stops processing and marks remaining non-transcribed sources as cancelled. " "Cancel stops processing and marks remaining non-transcribed sources as cancelled. "
"Cancelled sources can be resubmitted." "Cancelled sources can be resubmitted."
@@ -369,7 +369,7 @@ def register_page() -> None: # noqa: PLR0915
with archival_card(extra_classes="gap-2"): with archival_card(extra_classes="gap-2"):
ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary") ui.label(f"Job ID: {job.id}").classes("text-sm font-semibold font-mono ui-text-primary")
metadata_row("Current Status:", job.status.value) metadata_row("Current Status:", job.status.value.upper())
metadata_row("Resubmittable Sources:", str(resubmittable_count)) metadata_row("Resubmittable Sources:", str(resubmittable_count))
ui.label( ui.label(
"Resubmit queues failed and cancelled linked sources. " "Resubmit queues failed and cancelled linked sources. "
+7 -2
View File
@@ -98,8 +98,13 @@ def register_page() -> None: # noqa: PLR0915
rows = [ rows = [
PersonTableRow( PersonTableRow(
id=person.id, id=person.id,
last_name=person.last_name, name=f"{person.last_name}, {person.given_names}",
given_names=person.given_names, tags=", ".join(
sorted(
[link.tag_ref.label for link in person.person_tags if link.tag_ref is not None],
key=str.casefold,
)
),
family_search_id=person.family_search_id or "", family_search_id=person.family_search_id or "",
birth_date=compact_date(person.birth_date, person.birth_date_raw), birth_date=compact_date(person.birth_date, person.birth_date_raw),
death_date=compact_date(person.death_date, person.death_date_raw), death_date=compact_date(person.death_date, person.death_date_raw),
+1 -1
View File
@@ -456,7 +456,7 @@ def _render_source_job_metadata_zone(
status = latest_job_source.status.value status = latest_job_source.status.value
with ui.row().classes("w-full justify-between items-center mb-2"): with ui.row().classes("w-full justify-between items-center mb-2"):
ui.label("Latest Status").classes("text-xs ui-text-muted") ui.label("Latest Status").classes("text-xs ui-text-muted")
archival_badge(status) archival_badge(status.upper())
metadata_row("Job ID:", str(latest_job_source.job_id)) metadata_row("Job ID:", str(latest_job_source.job_id))
metadata_row( metadata_row(
+5
View File
@@ -36,6 +36,7 @@ async def seed_person_and_document():
name="Letter from Hig", name="Letter from Hig",
document_type_id=letter_type.id, document_type_id=letter_type.id,
archive_identifier="ZC-1924-001", archive_identifier="ZC-1924-001",
document_date=date(1924, 7, 4),
) )
session.add(doc) session.add(doc)
await session.flush() await session.flush()
@@ -148,6 +149,8 @@ class TestDocumentsPageRendering:
assert "Zenna Cochran" in response.text assert "Zenna Cochran" in response.text
assert "Document Type:" in response.text assert "Document Type:" in response.text
assert "Letter" in response.text assert "Letter" in response.text
assert "07-04-1924" in response.text
assert "1924-07-04" not in response.text
assert "PIPELINE JOBS" in response.text.upper() assert "PIPELINE JOBS" in response.text.upper()
assert "Edit Document" in response.text assert "Edit Document" in response.text
@@ -183,6 +186,8 @@ class TestDocumentsPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Edit Document Record" in response.text assert "Edit Document Record" in response.text
assert "Letter from Hig" in response.text assert "Letter from Hig" in response.text
assert "Document date" in response.text
assert "Exact date (YYYY-MM-DD)" not in response.text
assert "ZC-1924-001" in response.text assert "ZC-1924-001" in response.text
@pytest.mark.asyncio @pytest.mark.asyncio
+11 -5
View File
@@ -11,9 +11,11 @@ from transcription.db import session_scope
from transcription.db.models import Document from transcription.db.models import Document
from transcription.db.models import DocumentPerson from transcription.db.models import DocumentPerson
from transcription.db.models import Person from transcription.db.models import Person
from transcription.db.models import PersonTag
from transcription.db.models import PersonRole from transcription.db.models import PersonRole
from transcription.db.models import Photo from transcription.db.models import Photo
from transcription.db.models import Source from transcription.db.models import Source
from transcription.db.models import Tag
@pytest.mark.integration @pytest.mark.integration
@@ -35,16 +37,20 @@ class TestPeoplePageRendering:
_, client = app_client _, client = app_client
async with session_scope() as session: async with session_scope() as session:
session.add(Person(given_names="Ada", last_name="Lovelace")) person = Person(given_names="Ada", last_name="Lovelace")
tag = Tag(label="Pioneer", normalized_label="pioneer")
session.add_all([person, tag])
await session.flush()
session.add(PersonTag(person_id=person.id, tag_id=tag.id))
await session.commit() await session.commit()
response = client.get("/ui/people") response = client.get("/ui/people")
assert response.status_code == 200 assert response.status_code == 200
assert "Lovelace" in response.text assert "Lovelace, Ada" in response.text
assert "Ada" in response.text assert "Last Name, First &amp; Middle" in response.text or "Last Name, First & Middle" in response.text
assert "Last Name" in response.text assert "Tags" in response.text
assert "First &amp; Middle" in response.text or "First & Middle" in response.text assert "Pioneer" in response.text
assert "FamilySearch ID" in response.text assert "FamilySearch ID" in response.text
assert "# Documents" in response.text assert "# Documents" in response.text
+1 -1
View File
@@ -266,7 +266,7 @@ class TestSourcesPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "job-failed-page.png" in response.text assert "job-failed-page.png" in response.text
assert "failed" in response.text.lower() assert "FAILED" in response.text
assert "Provider timed out" in response.text assert "Provider timed out" in response.text
@pytest.mark.asyncio @pytest.mark.asyncio