UI updates, changes sync'd to UI docs

This commit is contained in:
Jim Lancaster
2026-08-02 18:20:38 -05:00
parent 9653060c2a
commit 0ab7ad50f2
22 changed files with 447 additions and 431 deletions
@@ -51,16 +51,16 @@ Entry points are indirect through user-facing entities:
### 4.2 Current Implementation ### 4.2 Current Implementation
Current entry point: service-level creation via DocumentService.create_document_person() Current entry point: Document create/edit flows
Current user action: no dedicated DocumentPerson UI Current user action: select an existing Person from the Document author dropdown
Current backend path: Document and Person workflows call DocumentPerson CRUD in DocumentService Current backend path: Document page submit callback -> `DocumentService.create_document_person()` or `delete_document_person()` as the author selection changes
| Field | Current Value at Create | Source | Visible to User | Evidence | | Field | Current Value at Create | Source | Visible to User | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
| id | Generated UUID | System | No | src/transcription/db/models.py | | id | Generated UUID | System | No | src/transcription/db/models.py |
| document_id | Caller-provided | Service/API caller | No | src/transcription/services/documents.py | | document_id | Caller-provided | Document UI | Indirectly | src/transcription/ui/pages/documents_page.py |
| person_id | Caller-provided | Service/API caller | No | src/transcription/services/documents.py | | person_id | Caller-provided | Document UI | Indirectly | src/transcription/ui/pages/documents_page.py |
| role | Caller-provided or default author | Service/model default | No | src/transcription/db/models.py, src/transcription/services/documents.py | | role | Default author in current UI | Service/model default | No | src/transcription/db/models.py, src/transcription/services/documents.py |
| created_at | Current UTC timestamp | System | No | src/transcription/db/models.py | | created_at | Current UTC timestamp | System | No | src/transcription/db/models.py |
### 4.3 Gap to Target ### 4.3 Gap to Target
@@ -178,5 +178,5 @@ Related user-facing workflows:
- Every DocumentPerson schema field appears in the field inventory. - Every DocumentPerson schema field appears in the field inventory.
- Intended behavior is defined as supporting workflow behavior rather than standalone UI. - Intended behavior is defined as supporting workflow behavior rather than standalone UI.
- Current behavior reflects service-level CRUD with no dedicated UI. - Current behavior reflects UI-backed CRUD through Document create/edit flows and Person detail rendering, with no standalone DocumentPerson UI.
- Gaps between intended and current behavior are explicit. - Gaps between intended and current behavior are explicit.
@@ -36,12 +36,12 @@ This checklist does not cover:
4. Then the page shows that linked person 4. Then the page shows that linked person
### RD-4 Sources section empty state ### RD-4 Sources section empty state
1. Given no related Source records 1. The page shows a Sources action for the current Document
2. Then the page shows no sources added yet and an action to add sources 2. The action routes to a document-scoped Sources view
### RD-5 Jobs section empty state ### RD-5 Jobs section empty state
1. Given no related Job records 1. The page shows a Jobs action for the current Document
2. Then the page shows no jobs created yet and an action to start or create jobs 2. The action routes to a document-scoped Jobs view
### RD-6 Filtered navigation readiness ### RD-6 Filtered navigation readiness
1. The detail page provides links or actions that can route to document-scoped Sources and Jobs views 1. The detail page provides links or actions that can route to document-scoped Sources and Jobs views
@@ -56,11 +56,12 @@ This checklist does not cover:
### UP-1 Edit entry ### UP-1 Edit entry
1. Given a loaded Document detail page 1. Given a loaded Document detail page
2. When the user chooses Edit document 2. When the user chooses Edit document
3. Then editable controls are shown for allowed fields only 3. Then editable controls are shown for allowed fields only, including the author relationship selector
### UP-2 Editable fields ### UP-2 Editable fields
1. Editable: name, document_type, document_date, document_date_raw, location_created, notes, archive_identifier 1. Editable: name, document_type, document_date, document_date_raw, location_created, notes, archive_identifier
2. Not editable: id, created_at, updated_at 2. Not editable: id, created_at, updated_at
3. The edit flow may also change the associated author Person link
### UP-3 Required validation ### UP-3 Required validation
1. name is required 1. name is required
+38 -41
View File
@@ -9,7 +9,7 @@ Acceptance criteria: acceptance-criteria.md
- Table: Document - Table: Document
- Primary key: `id` (UUID) - Primary key: `id` (UUID)
- Related entities: `Source`, `Job`, `DocumentPerson` - Related entities: `Source`, `Job`, `DocumentPerson`, `Person`
- Canonical schema references: - Canonical schema references:
- `src/transcription/db/models.py` - `src/transcription/db/models.py`
- `docs/schema_v2.md` - `docs/schema_v2.md`
@@ -26,8 +26,8 @@ This document uses three lenses:
| Field | DB Type | Nullable | Default/Auto Value | Intended UI Treatment | Notes | | Field | DB Type | Nullable | Default/Auto Value | Intended UI Treatment | Notes |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| id | UUID | No | `uuid4()` | Hidden, system-managed | Primary key | | id | UUID | No | `uuid4()` | Hidden, system-managed | Primary key |
| name | str | No | None | Shown, editable on create and future edit | Required | | name | str | No | None | Shown, editable on create and edit | Required |
| document_type | str | Yes | None | Shown, editable on create and future edit | Required by intended UX | | document_type | str | Yes | None | Shown, editable on create and edit | Required by intended UX |
| document_date | date | Yes | None | Shown, editable | Canonical exact date when present | | document_date | date | Yes | None | Shown, editable | Canonical exact date when present |
| document_date_raw | str | Yes | None | Shown, editable | Approximate or unknown date text | | document_date_raw | str | Yes | None | Shown, editable | Approximate or unknown date text |
| location_created | str | Yes | None | Shown, editable | Optional metadata | | location_created | str | Yes | None | Shown, editable | Optional metadata |
@@ -64,38 +64,37 @@ Related records during intended create:
### 4.2 Current Implementation ### 4.2 Current Implementation
Current entry point: upload page Current entry point: `/documents` page
Current user action: upload file via upload widget Current user action: open create form, fill metadata, optionally select an existing Person
Current backend path: upload page submit callback -> `create_upload_job()` -> `_create_upload_records()` Current backend path: document page submit callback -> `DocumentService.create_document()` -> optional `DocumentService.create_document_person()`
| Field | Current Value at Create | Source | Visible to User | Evidence | | Field | Current Value at Create | Source | Visible to User | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
| id | Generated UUID | System | No | `Document` default factory in `src/transcription/db/models.py` | | id | Generated UUID | System | No | `Document` default factory in `src/transcription/db/models.py` |
| name | Basename of uploaded filename | User file name transformed by service | Indirectly | Set in `src/transcription/services/store.py` | | name | User-provided | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| document_type | `None` | Service default | No | Not set in `src/transcription/services/store.py` | | document_type | User-provided or None | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| document_date | `None` | Service default | No | Not set in `src/transcription/services/store.py` | | document_date | Parsed from date input or None | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| document_date_raw | `None` | Service default | No | Not set in `src/transcription/services/store.py` | | document_date_raw | User-provided or None | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| location_created | `None` | Service default | No | Not set in `src/transcription/services/store.py` | | location_created | User-provided or None | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| notes | `None` | Service default | No | Not set in `src/transcription/services/store.py` | | notes | User-provided or None | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| archive_identifier | `None` | Service default | No | Not set in `src/transcription/services/store.py` | | archive_identifier | User-provided or None | User input | Yes | `src/transcription/ui/pages/documents_page.py` |
| created_at | Current UTC timestamp | System | No | Default factory in `src/transcription/db/models.py` | | created_at | Current UTC timestamp | System | No | Default factory in `src/transcription/db/models.py` |
| updated_at | Current UTC timestamp | System | No | Default factory in `src/transcription/db/models.py` | | updated_at | Current UTC timestamp | System | No | Default factory in `src/transcription/db/models.py` |
Current related-record behavior: Current related-record behavior:
- `Job` is created automatically. - User may optionally select an existing `Person`.
- `Source` is created automatically. - If selected, `DocumentPerson` is created with role `author`.
- `JobSource` is created automatically. - `Job` is not created during Document create.
- No `Person` or `DocumentPerson` records are created. - `Source` is not created during Document create.
### 4.3 Gap to Target ### 4.3 Gap to Target
To satisfy the intended Create flow, implementation must add: To satisfy the intended Create flow, implementation now includes:
1. a Document page and dedicated create form 1. a Document page and dedicated create form
2. user-entered metadata fields for `document_type`, `document_date`, `document_date_raw`, `location_created`, `notes`, and `archive_identifier` 2. user-entered metadata fields for `document_type`, `document_date`, `document_date_raw`, `location_created`, `notes`, and `archive_identifier`
3. optional Person lookup and inline Person creation 3. optional Person lookup through a dropdown of existing people
4. optional `DocumentPerson` link creation when a person is chosen 4. optional `DocumentPerson` link creation when a person is chosen
5. post-submit routing to a Document detail page 5. post-submit routing to a Document detail page
6. removal of the assumption that Document creation always starts with file upload
## 5. READ Mapping ## 5. READ Mapping
@@ -110,29 +109,27 @@ On the Document detail page, the user should be able to see:
### 5.2 Current Implementation ### 5.2 Current Implementation
Current Document visibility in the UI is indirect. Current Document visibility in the UI is direct.
| Field | Current Rendering | Visible to User | Notes | Evidence | | Field | Current Rendering | Visible to User | Notes | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
| name | Indirect filename context in jobs list and job detail | Yes, indirect | The UI shows source/job filename, not a dedicated Document page | `src/transcription/ui/pages/jobs_page.py`, `src/transcription/ui/components/table/jobs.py` | | name | Rendered as title and detail heading | Yes | Dedicated Document detail page | `src/transcription/ui/pages/documents_page.py` |
| id | Not shown as Document id | No | Job id is shown instead | `src/transcription/ui/pages/jobs_page.py` | | id | Not shown as raw id | No | Internal identifier remains hidden | `src/transcription/ui/pages/documents_page.py` |
| document_type | Not rendered | No | Hidden metadata | no current UI field | | document_type | Rendered | Yes | Shown on detail and editable on create/edit | `src/transcription/ui/pages/documents_page.py` |
| document_date | Not rendered | No | Hidden metadata | no current UI field | | document_date | Rendered | Yes | Exact date shown when present | `src/transcription/ui/pages/documents_page.py` |
| document_date_raw | Not rendered | No | Hidden metadata | no current UI field | | document_date_raw | Rendered | Yes | Approximate date shown when present | `src/transcription/ui/pages/documents_page.py` |
| location_created | Not rendered | No | Hidden metadata | no current UI field | | location_created | Rendered | Yes | Optional metadata shown | `src/transcription/ui/pages/documents_page.py` |
| notes | Not rendered | No | Hidden metadata | no current UI field | | notes | Rendered | Yes | Optional metadata shown | `src/transcription/ui/pages/documents_page.py` |
| archive_identifier | Not rendered | No | Hidden metadata | no current UI field | | archive_identifier | Rendered | Yes | Optional metadata shown | `src/transcription/ui/pages/documents_page.py` |
| created_at | Not rendered as Document timestamp | No | Job timestamps are shown instead | `src/transcription/ui/components/table/jobs.py` | | created_at | Rendered read-only | Yes | System timestamp shown on detail | `src/transcription/ui/pages/documents_page.py` |
| updated_at | Not rendered as Document timestamp | No | Job metadata is shown instead | `src/transcription/ui/components/transcript.py` | | updated_at | Rendered read-only | Yes | System timestamp shown on detail | `src/transcription/ui/pages/documents_page.py` |
### 5.3 Gap to Target ### 5.3 Gap to Target
To satisfy the intended Read flow, implementation must add: To satisfy the intended Read flow, implementation now includes:
1. a Document detail page 1. metadata rendering for Document fields
2. metadata rendering for Document fields 2. linked people rendering
3. linked people rendering 3. document-scoped Sources and Jobs navigation views
4. Sources and Jobs sections with empty states
5. filtered navigation from the detail page into document-specific Jobs and Sources views
## 6. UPDATE Mapping ## 6. UPDATE Mapping
@@ -171,10 +168,10 @@ Intended system-managed fields:
### 6.3 Gap to Target ### 6.3 Gap to Target
Implementation must add: Implementation now includes:
1. Document edit controls in the UI 1. Document edit controls in the UI
2. validation and save behavior for Document metadata 2. validation and save behavior for Document metadata
3. a consistent `updated_at` update policy if metadata edits are introduced 3. author relationship controls through the edit flow
## 7. DELETE Mapping ## 7. DELETE Mapping
@@ -191,11 +188,11 @@ Rules:
| Action | UI Exposed | Backend Capability | Notes | | Action | UI Exposed | Backend Capability | Notes |
|---|---|---|---| |---|---|---|---|
| Delete Document | No | Yes | `DocumentService.delete_document()` exists, but no dedicated UI guard flow exists | | Delete Document | Yes | Yes | `DocumentService.delete_document()` exists and the UI blocks dependent deletes |
### 7.3 Gap to Target ### 7.3 Gap to Target
Implementation must add: Implementation includes:
1. a Document delete control in the UI 1. a Document delete control in the UI
2. pre-delete dependency checks for Jobs and Sources 2. pre-delete dependency checks for Jobs and Sources
3. user-facing messaging when deletion is blocked 3. user-facing messaging when deletion is blocked
+16 -17
View File
@@ -15,9 +15,9 @@ Creating a Document is a metadata-first workflow:
1. The user opens the Document page. 1. The user opens the Document page.
2. The user selects Create new document. 2. The user selects Create new document.
3. The user enters descriptive metadata about the document. 3. The user enters descriptive metadata about the document.
4. The user optionally links one related person. 4. The user optionally selects one related person from the existing Person list.
5. The system creates the Document. 5. The system creates the Document.
6. If a person was selected or created, the system links that Person to the Document through DocumentPerson. 6. If a person was selected, the system links that Person to the Document through DocumentPerson with author role.
7. The user sees a success state and lands on the new Document detail page. 7. The user sees a success state and lands on the new Document detail page.
## 2. User Goal ## 2. User Goal
@@ -46,9 +46,9 @@ The Document detail page is the page for one specific Document after it has been
It should show: It should show:
1. the Document metadata 1. the Document metadata
2. related people linked to the Document 2. related people linked to the Document
3. a Sources section 3. a linked-author summary when available
4. a Jobs section 4. document-scoped navigation links for Sources and Jobs
5. empty states when no sources or jobs exist yet 5. filtered views for sources and jobs linked to the current document
It should later support links to filtered views for: It should later support links to filtered views for:
1. jobs associated with the current document only 1. jobs associated with the current document only
@@ -67,7 +67,8 @@ Expected UI affordance:
Preferred first implementation: Preferred first implementation:
1. A dedicated Document create page or panel. 1. A dedicated Document create page or panel.
2. A simple form with explicit labels. 2. A simple form with explicit labels.
3. Text inputs are acceptable for first release, even where future versions may use dropdowns or richer selectors. 3. Existing Person records should be selectable through a dropdown.
4. Text inputs are acceptable for the remaining fields in first release.
## 5. Create Document Form ## 5. Create Document Form
@@ -124,12 +125,13 @@ The Create Document flow may optionally link one related person during first rel
| UI Label | Schema Area | Input Type | Required | Notes | | UI Label | Schema Area | Input Type | Required | Notes |
|---|---|---|---|---| |---|---|---|---|---|
| Related person | Person -> DocumentPerson | Search/select or create inline | No | Intended to support common author-like associations without making the field mandatory | | Related person | Person -> DocumentPerson | Dropdown select | No | Selects an existing Person and links as author when saved |
First release behavior: First release behavior:
1. The user may save a Document without linking any person. 1. The user may save a Document without linking any person.
2. If a person is linked during create, only one person is supported in first release. 2. If a person is linked during create, only one person is supported in first release.
3. Additional people and recipient workflows are deferred to a future revision. 3. The selected person is linked as author.
4. Additional people and recipient workflows are deferred to a future revision.
### 5.6 Related Records Not Created Directly Here ### 5.6 Related Records Not Created Directly Here
@@ -144,7 +146,7 @@ First release behavior:
The user should be able to: The user should be able to:
1. select an existing Person to associate with the Document 1. select an existing Person to associate with the Document
2. create a new Person if the person does not already exist 2. change the associated Person from the Document edit flow
3. save the Document even if no person is linked 3. save the Document even if no person is linked
### 6.2 Data Model Interpretation ### 6.2 Data Model Interpretation
@@ -162,16 +164,13 @@ This means:
### 6.3 Related Person UI Behavior ### 6.3 Related Person UI Behavior
Minimum acceptable first implementation: Minimum acceptable first implementation:
1. Searchable or scrollable list of existing Person records. 1. Dropdown of existing Person records.
2. Option to create a new Person inline or in a small secondary flow. 2. Clear display of the selected related person before submit.
3. Clear display of the selected related person before submit. 3. Ability to change or clear the selected person in the Document edit flow.
If the person does not exist: If the person does not exist:
1. User selects Create new person. 1. The user should create the Person first from the Person page.
2. User enters the minimum required Person information. 2. The Document create flow only links existing Person records in first release.
3. System creates Person.
4. System returns to Document create flow.
5. System links the new Person if the user completes Document creation.
## 7. Validation Rules ## 7. Validation Rules
+13 -31
View File
@@ -76,12 +76,12 @@ This checklist does not cover:
1. Given a valid job id 1. Given a valid job id
2. When the user opens Job detail 2. When the user opens Job detail
3. Then job metadata for that record only is shown 3. Then job metadata for that record only is shown
4. Then source preview or source warning state is shown 4. Then document-scoped navigation links for Sources and Jobs are shown
### RD-4 Detail execution context visibility ### RD-4 Detail execution context visibility
1. provider, model, and prompt_name are displayed when known 1. provider, model, and prompt_name are displayed when known
2. status lifecycle value is visible 2. status lifecycle value is visible
3. source-level transcription and revision context is visible 3. source-level transcription and revision context remains deferred in the current UI
### RD-5 Missing and invalid id states ### RD-5 Missing and invalid id states
1. Given an invalid job id format 1. Given an invalid job id format
@@ -92,25 +92,18 @@ This checklist does not cover:
## Update Acceptance Criteria ## Update Acceptance Criteria
### UP-1 Revision edit entry ### UP-1 Revision edit entry
1. Given a job detail page with source context 1. Given a job detail page
2. When the user enters revision edit flow 2. When the user opens the page
3. Then revised_text input is available 3. Then document-scoped navigation links are available
### UP-2 Revision validation ### UP-2 Revision validation
1. revised_text cannot be saved as empty after trimming 1. source revision editing remains deferred in the current UI
2. Warning feedback is shown for invalid empty input
### UP-3 Successful revision save ### UP-3 Successful revision save
1. Given valid revision text 1. deferred until the source revision UI is restored
2. When the user saves
3. Then revised_text persists
4. Then success feedback is shown
5. Then refreshed revision content is visible
### UP-4 Revision save failure ### UP-4 Revision save failure
1. Given backend failure during revision save 1. deferred until the source revision UI is restored
2. Then clear error feedback is shown
3. Then the user-entered text remains available for retry where possible
### UP-5 Job lifecycle state update visibility ### UP-5 Job lifecycle state update visibility
1. status changes from queued to processing to terminal states are reflected in UI 1. status changes from queued to processing to terminal states are reflected in UI
@@ -120,30 +113,19 @@ This checklist does not cover:
## Delete Acceptance Criteria ## Delete Acceptance Criteria
### DL-1 Delete entry and confirmation ### DL-1 Delete entry and confirmation
1. Given a job detail context with delete affordance 1. job deletion is deferred in the current UI
2. When the user selects delete job
3. Then a permanent-action confirmation dialog appears
### DL-2 Dependency guardrails ### DL-2 Dependency guardrails
1. If deletion policy requires retention handling for related JobSource history, delete is blocked 1. deferred until a job delete flow is reintroduced
2. If deletion policy allows dependent cleanup path, delete can proceed
### DL-3 Blocked delete behavior ### DL-3 Blocked delete behavior
1. When blocked 1. deferred until a job delete flow is reintroduced
2. Then UI explains dependency constraints
3. Then UI provides cleanup guidance
### DL-4 Successful delete ### DL-4 Successful delete
1. Given no blocking dependencies 1. deferred until a job delete flow is reintroduced
2. When the user confirms deletion
3. Then job is removed
4. Then success feedback is shown
5. Then the user returns to Jobs list
### DL-5 Delete failure ### DL-5 Delete failure
1. Given backend failure during delete 1. deferred until a job delete flow is reintroduced
2. Then clear error feedback is shown
3. Then the user remains in Job context with retry path
## Cross-Criteria Quality Gates ## Cross-Criteria Quality Gates
+20 -26
View File
@@ -36,11 +36,7 @@ This document uses three lenses:
| prompt_name | str | Yes | None | Visible when known; editable if create-time options are available | Prompt metadata | | prompt_name | str | Yes | None | Visible when known; editable if create-time options are available | Prompt metadata |
Related execution fields rendered in Job detail via relationships: Related execution fields rendered in Job detail via relationships:
- JobSource.status - none in the current simplified detail view beyond job metadata and document navigation links
- JobSource.error_detail
- JobSource.executed_at
- Source.upload_name, Source.filename, Source.page_number
- Source.raw_transcription, Source.revised_text
## 4. CREATE Mapping ## 4. CREATE Mapping
@@ -69,9 +65,9 @@ Create-related relationship rules:
### 4.2 Current Implementation ### 4.2 Current Implementation
Current entry point: upload page through create_upload_job() Current entry point: Jobs page create flow
Current user action: upload one file through upload flow Current user action: select Document and upload one or more files or a folder through a single upload widget
Current backend path: upload submit -> create_upload_job -> _create_upload_records Current backend path: job create submit -> create_job_for_document()
| Field | Current Value at Create | Source | Visible to User | Evidence | | Field | Current Value at Create | Source | Visible to User | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
@@ -81,14 +77,14 @@ Current backend path: upload submit -> create_upload_job -> _create_upload_recor
| retry_count | 0 | Model default | Yes | src/transcription/db/models.py, src/transcription/ui/pages/jobs_page.py | | retry_count | 0 | Model default | Yes | src/transcription/db/models.py, src/transcription/ui/pages/jobs_page.py |
| date_created | current UTC timestamp | System | Yes | src/transcription/db/models.py, src/transcription/ui/pages/jobs_page.py | | date_created | current UTC timestamp | System | Yes | src/transcription/db/models.py, src/transcription/ui/pages/jobs_page.py |
| date_updated | current UTC timestamp | System | Yes | src/transcription/db/models.py, src/transcription/ui/pages/jobs_page.py | | date_updated | current UTC timestamp | System | Yes | src/transcription/db/models.py, src/transcription/ui/pages/jobs_page.py |
| provider | None at create, set after transcription update | Workflow/service | Partially | src/transcription/services/workflows.py | | provider | None at create, set after transcription update | Workflow/service | Yes | src/transcription/services/workflows.py |
| model | None at create, set after transcription update | Workflow/service | Partially | src/transcription/services/workflows.py | | model | None at create, set after transcription update | Workflow/service | Yes | src/transcription/services/workflows.py |
| prompt_name | None at create, set by workflow updates | Workflow/service | Partially | src/transcription/services/workflows.py | | prompt_name | None at create, set by workflow updates | Workflow/service | Yes | src/transcription/services/workflows.py |
Current create constraints: Current create constraints:
1. no dedicated Create job action in the Jobs page. 1. dedicated Create job action exists in the Jobs page.
2. upload flow currently creates Document, Job, Source, and JobSource together. 2. job create flow requires a Document selection.
3. current upload path accepts a single file per submission. 3. current upload path accepts one widget for files or folder selection.
### 4.3 Gap to Target ### 4.3 Gap to Target
@@ -132,16 +128,14 @@ Source-related read behavior:
### 5.3 Gap to Target ### 5.3 Gap to Target
To satisfy intended Read flow, implementation must add: To satisfy intended Read flow, implementation must add:
1. list-level create affordance and enhanced filtering/search UX. 1. richer per-source detail navigation if a later revision restores transcription review in Job detail.
2. explicit Document context rendering in list and detail. 2. richer filtering/search UX if needed.
3. explicit provider/model/prompt_name rendering in detail when known.
4. richer multi-source detail navigation when more than one source is linked.
## 6. UPDATE Mapping ## 6. UPDATE Mapping
### 6.1 Intended Update Behavior ### 6.1 Intended Update Behavior
Primary user updates in first release are revision edits in job detail source context. Primary user updates in first release are source revision edits in job detail source context in the product plan, but the current UI no longer exposes that surface.
Intended editable scope (first release): Intended editable scope (first release):
- Source.revised_text through Job detail review - Source.revised_text through Job detail review
@@ -162,7 +156,7 @@ Job metadata visibility policy:
| Field/Area | Updatable via UI | Updatable via Service | Notes | | Field/Area | Updatable via UI | Updatable via Service | Notes |
|---|---|---|---| |---|---|---|---|
| Source.revised_text from job detail | Yes | Yes | Saved via transcription service revision path | | Source.revised_text from job detail | No | Yes | Saved via transcription service revision path, but the current UI does not expose the editor |
| status | No | Yes | Updated by workflow lifecycle services | | status | No | Yes | Updated by workflow lifecycle services |
| retry_count | No | Yes | Incremented by workflow retry logic | | retry_count | No | Yes | Incremented by workflow retry logic |
| provider/model/prompt_name | No | Yes | Set during transcription result finalization | | provider/model/prompt_name | No | Yes | Set during transcription result finalization |
@@ -170,16 +164,16 @@ Job metadata visibility policy:
### 6.3 Gap to Target ### 6.3 Gap to Target
Implementation should add: Implementation now includes:
1. explicit create-mode handling for provider/model/prompt visibility and optional selection. 1. create-mode handling for provider/model/prompt visibility and optional selection.
2. richer detail display for provider/model/prompt and source-level execution outcomes. 2. detail display for provider/model/prompt and document-scoped navigation links.
3. optional future manual controls for retry and state transitions. 3. manual controls for retry and state transitions remain deferred.
## 7. DELETE Mapping ## 7. DELETE Mapping
### 7.1 Intended Delete Behavior ### 7.1 Intended Delete Behavior
Job deletion should include dependency-aware guardrails. Job deletion is deferred in the current UI.
Rules: Rules:
1. deletion is allowed only when policy allows cleanup or retention handling for related JobSource records. 1. deletion is allowed only when policy allows cleanup or retention handling for related JobSource records.
@@ -194,7 +188,7 @@ Rules:
### 7.3 Gap to Target ### 7.3 Gap to Target
Implementation must add: Implementation should add in a future revision:
1. delete control in Job detail. 1. delete control in Job detail.
2. dependency checks and blocked-delete messaging. 2. dependency checks and blocked-delete messaging.
3. success navigation and confirmation UX. 3. success navigation and confirmation UX.
+5 -4
View File
@@ -18,7 +18,7 @@ Managing a Job is run-first:
4. The user links a Document and uploads one or more source files. 4. The user links a Document and uploads one or more source files.
5. The user submits for transcription. 5. The user submits for transcription.
6. The system creates and processes the Job. 6. The system creates and processes the Job.
7. The user reviews per-source output and saves revisions. 7. The user reviews job metadata and follows document-scoped links for Sources and Jobs.
## 2. User Goal ## 2. User Goal
@@ -54,9 +54,9 @@ Create mode should include:
Detail mode should include: Detail mode should include:
1. job metadata and status 1. job metadata and status
2. per-source processing state 2. document-scoped navigation links for the current Document
3. original transcription output 3. provider/model/prompt visibility when known
4. revision editor and save actions 4. no delete action in first release
## 4. Entry Points ## 4. Entry Points
@@ -67,6 +67,7 @@ Primary entry points:
Current implementation note: Current implementation note:
1. current code path is upload-first and auto-creates Job records 1. current code path is upload-first and auto-creates Job records
2. intended UX is explicit Create job from the Jobs page 2. intended UX is explicit Create job from the Jobs page
3. current detail view is link-oriented and does not show per-source transcription or revision editing controls
## 5. Create Job Flow ## 5. Create Job Flow
@@ -59,6 +59,7 @@ This checklist does not cover:
### RD-2 Metadata visibility ### RD-2 Metadata visibility
1. The page shows full_name and available optional person fields 1. The page shows full_name and available optional person fields
2. created_at and updated_at are shown as system-managed, read-only values 2. created_at and updated_at are shown as system-managed, read-only values
3. portrait_path is rendered when available, including an image preview when possible
### RD-3 Linked documents section ### RD-3 Linked documents section
1. Given no linked DocumentPerson rows 1. Given no linked DocumentPerson rows
+7 -8
View File
@@ -117,7 +117,7 @@ Current Person visibility in the UI is not implemented as a dedicated page.
| Field | Current Rendering | Visible to User | Notes | Evidence | | Field | Current Rendering | Visible to User | Notes | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
| full_name | Not rendered in dedicated Person UI | No | No current Person page | no Person page in src/transcription/ui | | full_name | Rendered in header and summary | Yes | Dedicated Person page exists | `src/transcription/ui/pages/people_page.py` |
| display_name | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | display_name | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
| maiden_name | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | maiden_name | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
| birth_date | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | birth_date | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
@@ -127,18 +127,17 @@ Current Person visibility in the UI is not implemented as a dedicated page.
| death_date_raw | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | death_date_raw | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
| death_place | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | death_place | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
| biography | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | biography | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
| portrait_path | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | portrait_path | Rendered as text and image when available | Yes | Dedicated Person page exists | `src/transcription/ui/pages/people_page.py` |
| metadata_ | Not rendered | No | Hidden advanced field | no current UI field | | metadata_ | Not rendered | No | Hidden advanced field | no current UI field |
| created_at | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | created_at | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
| updated_at | Not rendered | No | No current Person page | no Person page in src/transcription/ui | | updated_at | Not rendered | No | No current Person page | no Person page in src/transcription/ui |
### 5.3 Gap to Target ### 5.3 Gap to Target
To satisfy the intended Read flow, implementation must add: To satisfy the intended Read flow, implementation now includes:
1. a Person detail page 1. metadata rendering for Person fields
2. metadata rendering for Person fields 2. linked Documents section with empty states
3. linked Documents section with empty states 3. document-link navigation paths
4. document-link navigation paths
## 6. UPDATE Mapping ## 6. UPDATE Mapping
@@ -189,7 +188,7 @@ Hidden in first release:
### 6.3 Gap to Target ### 6.3 Gap to Target
Implementation must add: Implementation now includes:
1. Person edit controls in the UI 1. Person edit controls in the UI
2. validation and save behavior for Person metadata 2. validation and save behavior for Person metadata
3. a consistent updated_at update policy for Person edits 3. a consistent updated_at update policy for Person edits
+3 -2
View File
@@ -44,8 +44,9 @@ The Person detail page is the page for one specific Person after creation.
It should show: It should show:
1. core identity fields 1. core identity fields
2. biographical metadata 2. biographical metadata
3. related Documents section 3. portrait image when available
4. empty state when no linked documents exist yet 4. related Documents section
5. empty state when no linked documents exist yet
## 4. Entry Point ## 4. Entry Point
+11 -11
View File
@@ -59,14 +59,14 @@ This checklist does not cover:
## Read Acceptance Criteria ## Read Acceptance Criteria
### RD-1 Source detail retrieval ### RD-1 Source detail retrieval
1. Given a valid Source id in source context, typically via job detail in first release 1. Given a valid Source id in source context, typically via a document-scoped source list in first release
2. When the user opens source detail or equivalent panel 2. When the user opens source detail or equivalent panel
3. Then source metadata and preview are displayed for that source only 3. Then source metadata and preview are displayed for that source only if the detail UI exists in a later revision
### RD-2 Transcription and revision visibility ### RD-2 Transcription and revision visibility
1. Original transcription context is visible read-only 1. Original transcription context is visible read-only when the revision UI exists
2. Revision state is visible 2. Revision state is visible when the revision UI exists
3. If revised_text is absent, no revision yet messaging is shown 3. If revised_text is absent, no revision yet messaging is shown when the revision UI exists
### RD-3 Missing source state ### RD-3 Missing source state
1. Given a missing source 1. Given a missing source
@@ -77,29 +77,29 @@ This checklist does not cover:
### UP-1 Revision editing entry ### UP-1 Revision editing entry
1. Given a source context 1. Given a source context
2. When the user enters revision edit flow 2. When the user enters revision edit flow
3. Then revised_text input is available 3. Then revised_text input is available in a later revision of the UI
### UP-2 Revision validation ### UP-2 Revision validation
1. revised_text cannot be saved as empty after trimming 1. revised_text cannot be saved as empty after trimming when the revision UI exists
2. Warning feedback is shown for invalid empty input 2. Warning feedback is shown for invalid empty input when the revision UI exists
### UP-3 Successful revision save ### UP-3 Successful revision save
1. Given valid revision text 1. Given valid revision text
2. When the user saves 2. When the user saves in a later revision of the UI
3. Then revised_text persists 3. Then revised_text persists
4. Then date_revised is updated 4. Then date_revised is updated
5. Then success feedback is shown 5. Then success feedback is shown
6. Then refreshed revision content is visible 6. Then refreshed revision content is visible
### UP-4 Revision save failure ### UP-4 Revision save failure
1. Given backend failure during save 1. Given backend failure during save in a later revision of the UI
2. Then clear error feedback is shown 2. Then clear error feedback is shown
3. Then the user-entered text remains available for retry where possible 3. Then the user-entered text remains available for retry where possible
## Delete Acceptance Criteria ## Delete Acceptance Criteria
### DL-1 Delete entry and confirmation ### DL-1 Delete entry and confirmation
1. Given a source in source context, typically via job detail in first release 1. Given a source in source context, typically via a later source detail view
2. When the user selects delete source 2. When the user selects delete source
3. Then a permanent-action confirmation dialog appears 3. Then a permanent-action confirmation dialog appears
+22 -23
View File
@@ -31,8 +31,8 @@ This document uses three lenses:
| upload_name | str | No | None | Shown read-only after upload | Original user-provided name | | upload_name | str | No | None | Shown read-only after upload | Original user-provided name |
| filename | str | No | None | Shown read-only | Stored filename | | filename | str | No | None | Shown read-only | Stored filename |
| file_path | str | No | None | Usually hidden; preview uses path internally | Filesystem path | | file_path | str | No | None | Usually hidden; preview uses path internally | Filesystem path |
| raw_transcription | str | Yes | None | Shown read-only | Immutable machine output context | | raw_transcription | str | Yes | None | Shown indirectly or hidden | Immutable machine output context |
| revised_text | str | Yes | None | Shown editable | Human-authored correction | | revised_text | str | Yes | None | Hidden in current simplified UI | Human-authored correction |
| date_uploaded | datetime | No | datetime.now(UTC) | Shown read-only | System-managed timestamp | | date_uploaded | datetime | No | datetime.now(UTC) | Shown read-only | System-managed timestamp |
| date_revised | datetime | Yes | None | Shown read-only | Set when revision is saved | | date_revised | datetime | Yes | None | Shown read-only | Set when revision is saved |
@@ -59,9 +59,9 @@ Success destination: source preview or revision flow in job detail context
### 4.2 Current Implementation ### 4.2 Current Implementation
Current entry point: upload page through create_upload_job() Current entry point: Jobs page create flow
Current user action: upload one file through upload flow Current user action: upload one or more files or a folder through a single upload widget
Current backend path: upload submit -> create_upload_job -> _create_upload_records Current backend path: job create submit -> create_job_for_document()
| Field | Current Value at Create | Source | Visible to User | Evidence | | Field | Current Value at Create | Source | Visible to User | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
@@ -96,34 +96,33 @@ On Source detail/list surfaces, users should be able to see:
### 5.2 Current Implementation ### 5.2 Current Implementation
Current Source reading is primarily embedded in job detail. Current Source reading is primarily embedded in a document-scoped source list and job create/navigation flow.
| Field | Current Rendering | Visible to User | Notes | Evidence | | Field | Current Rendering | Visible to User | Notes | Evidence |
|---|---|---|---|---| |---|---|---|---|---|
| upload_name | Shown in source-related UI | Yes | Displayed in job detail source context | src/transcription/ui/components/job_detail.py | | upload_name | Shown in document-scoped source list | Yes | Displayed in source list context | src/transcription/ui/pages/documents_page.py |
| filename | Indirectly shown in jobs list/detail | Yes | Via Job.filename and source metadata | src/transcription/ui/components/table/jobs.py, src/transcription/ui/pages/jobs_page.py | | filename | Shown in document-scoped source list | Yes | Source metadata shown in list | src/transcription/ui/pages/documents_page.py |
| file_path | Used for preview rendering | Indirectly | Source preview in job detail page | src/transcription/ui/pages/jobs_page.py | | file_path | Hidden from current simplified UI | No | Operational path remains internal | current UI pages |
| page_number | Not explicitly rendered in current jobs page flow | Limited | Ordering implicit via selected source | jobs page flow | | page_number | Shown in document-scoped source list | Yes | Ordering visible in document source list | src/transcription/ui/pages/documents_page.py |
| raw_transcription | Rendered from JobSource context | Yes | Original transcription card | src/transcription/ui/components/transcript.py | | raw_transcription | Not shown in current simplified UI | No | Source preview/revision UI deferred | current UI pages |
| revised_text | Rendered/editable in revision editor | Yes | Revision editor in job detail page | src/transcription/ui/pages/jobs_page.py | | revised_text | Not shown in current simplified UI | No | Source revision UI deferred | current UI pages |
| date_uploaded | Limited visibility | Partial | Shown in some source metadata components | src/transcription/ui/components/job_detail.py | | date_uploaded | Not shown in current simplified UI | No | Operational metadata only | current UI pages |
| date_revised | Indirect via revision row timestamp | Partial | Used in revision caption | src/transcription/ui/components/transcript.py | | date_revised | Not shown in current simplified UI | No | Operational metadata only | current UI pages |
### 5.3 Gap to Target ### 5.3 Gap to Target
To satisfy intended Read flow, implementation must add: To satisfy intended Read flow, implementation must add:
1. dedicated Source list and detail surfaces under Document context 1. richer source detail and revision surfaces if the review workflow returns later
2. explicit page_number presentation and navigation 2. optional page-specific navigation if needed beyond the document-scoped list
3. clearer source metadata display independent of Job-centric pages
## 6. UPDATE Mapping ## 6. UPDATE Mapping
### 6.1 Intended Update Behavior ### 6.1 Intended Update Behavior
Primary user update for Source is revised_text maintenance. Primary user update for Source is revised_text maintenance in the service layer; the current UI does not expose this flow.
Intended editable fields (first release): Intended editable fields (first release):
- revised_text - revised_text, if the revision UI is restored later
Intended read-only fields (first release): Intended read-only fields (first release):
- document_id - document_id
@@ -139,13 +138,13 @@ Intended read-only fields (first release):
| Field | Updatable via UI | Updatable via Service | Notes | | Field | Updatable via UI | Updatable via Service | Notes |
|---|---|---|---| |---|---|---|---|
| revised_text | Yes | Yes | Saved via TranscriptionService.upsert_revision_for_source() | | revised_text | No | Yes | Saved via TranscriptionService.upsert_revision_for_source(), but not exposed in current UI |
| date_revised | No | Yes | Set automatically on revision save | | date_revised | No | Yes | Set automatically on revision save |
| other fields | No | Technically yes in service layer | No first-class UI editing flow | | other fields | No | Technically yes in service layer | No first-class UI editing flow |
### 6.3 Gap to Target ### 6.3 Gap to Target
Implementation should add: Implementation should add in a later revision:
1. explicit Source edit boundaries in dedicated Source UI 1. explicit Source edit boundaries in dedicated Source UI
2. validation and save UX for revisions in document-scoped source views 2. validation and save UX for revisions in document-scoped source views
3. optional future controls for page ordering and metadata corrections 3. optional future controls for page ordering and metadata corrections
@@ -154,7 +153,7 @@ Implementation should add:
### 7.1 Intended Delete Behavior ### 7.1 Intended Delete Behavior
Source deletion should include dependency-aware guardrails. Source deletion is deferred in the current UI.
Rules: Rules:
1. Deletion can proceed when policy allows cleanup of related JobSource records. 1. Deletion can proceed when policy allows cleanup of related JobSource records.
@@ -168,7 +167,7 @@ Rules:
### 7.3 Gap to Target ### 7.3 Gap to Target
Implementation must add: Implementation should add in a future revision:
1. source delete controls in source/document context UI 1. source delete controls in source/document context UI
2. dependency checks for JobSource links 2. dependency checks for JobSource links
3. blocked-delete messaging and cleanup path guidance 3. blocked-delete messaging and cleanup path guidance
+22 -31
View File
@@ -15,7 +15,7 @@ Managing Source records is page-first:
1. The user starts from a transcription job flow. 1. The user starts from a transcription job flow.
2. The user adds one or more source files. 2. The user adds one or more source files.
3. The system creates Source records linked to the Document and linked to the Job through JobSource. 3. The system creates Source records linked to the Document and linked to the Job through JobSource.
4. The user reviews original transcription output and can maintain revised text. 4. The user reviews source lists by document and can navigate to the owning Document or Job.
## 2. User Goal ## 2. User Goal
@@ -34,15 +34,11 @@ The user wants to:
A Source list surface should support: A Source list surface should support:
1. listing source pages for a selected Document 1. listing source pages for a selected Document
2. sorting by page_number 2. sorting by page_number
3. opening a source detail or revision view 3. opening the owning Document or Job context
### 3.2 Source Detail Surface ### 3.2 Source Detail Surface
A Source detail surface should show: Source detail and revision surfaces are deferred in the current UI.
1. file preview (image or PDF)
2. source metadata
3. original transcription context
4. revision editor and revision state
## 4. Entry Points ## 4. Entry Points
@@ -51,7 +47,7 @@ Primary entry points:
2. from Job detail, open source preview and revision editor 2. from Job detail, open source preview and revision editor
Current implementation note: Current implementation note:
1. source interaction currently occurs in upload and job-detail flows 1. source interaction currently occurs in job-create and document-scoped source list flows
## 5. Create Source Flow ## 5. Create Source Flow
@@ -106,7 +102,7 @@ After successful source create:
1. Source is linked to the Document 1. Source is linked to the Document
2. Source appears in page order derived from alphabetical upload filename ordering 2. Source appears in page order derived from alphabetical upload filename ordering
3. Source is linked to the Job through JobSource at create time 3. Source is linked to the Job through JobSource at create time
4. The user can open preview and revision workflow 4. The user can open the owning Document or Job context
### 5.8 Source Creation Invariant ### 5.8 Source Creation Invariant
@@ -119,40 +115,35 @@ For first release:
### 6.1 User Intent ### 6.1 User Intent
The user wants to view each page file and understand file identity, processing context, and revision status. The user wants to view each page file and understand file identity and processing context.
### 6.2 Read Surface Expectations ### 6.2 Read Surface Expectations
The UI should show: The UI should show:
1. file preview for the current Source 1. source lists grouped by Document
2. upload_name as the original user-provided filename 2. upload_name as the original user-provided filename
3. filename as the stored system filename 3. filename as the stored system filename
4. file_path information, with the option to keep it internal or partially hidden for safety 4. page_number and ordering context
5. page_number and ordering context 5. the owning Document and Job navigation context
6. whether revised_text exists
7. original transcription output context
### 6.3 Read Empty and Missing States ### 6.3 Read Empty and Missing States
If source is missing: If source is missing:
1. Show clear not found or no source available messaging 1. Show clear not found or no source available messaging
If source has no revision:
1. Show empty-state guidance and create-revision action
If source metadata is partially unavailable: If source metadata is partially unavailable:
1. Show fallback labels and keep preview and revision actions available where possible 1. Show fallback labels and keep navigation available where possible
## 7. Update Source Journey ## 7. Update Source Journey
### 7.1 User Intent ### 7.1 User Intent
The user primarily updates page-level revision text while preserving raw machine output. The user primarily tracks page-level source records while preserving raw machine output in the service layer.
### 7.2 Intended Editable Fields ### 7.2 Intended Editable Fields
Editable in first release: Editable in first release:
1. revised_text 1. revised_text in a later revision, not in the current simplified UI
Read-only in first release: Read-only in first release:
1. upload_name 1. upload_name
@@ -166,11 +157,11 @@ Read-only in first release:
### 7.3 Revision Save Behavior ### 7.3 Revision Save Behavior
On save: On save:
1. validate revision text is non-empty 1. validate revision text is non-empty when the revision UI exists in a later release
2. persist revised_text 2. persist revised_text when the revision UI exists
3. set date_revised 3. set date_revised when the revision UI exists
4. show success feedback 4. show success feedback when the revision UI exists
5. keep user in current source context 5. keep user in current source context when the revision UI exists
### 7.4 Revision Failure Behavior ### 7.4 Revision Failure Behavior
@@ -196,13 +187,13 @@ Delete is blocked when:
### 8.3 Delete UX ### 8.3 Delete UX
When blocked: When blocked:
1. explain dependency constraints 1. explain dependency constraints in a future delete flow
2. Show cleanup guidance 2. show cleanup guidance in a future delete flow
When allowed: When allowed:
1. confirm permanent removal 1. confirm permanent removal in a future delete flow
2. remove source 2. remove source in a future delete flow
3. return to source list with success state 3. return to source list with success state in a future delete flow
## 9. Relationship to Other Workflows ## 9. Relationship to Other Workflows
+11 -11
View File
@@ -14,8 +14,8 @@ Status legend:
| Criteria Group | Acceptance IDs | Status | Primary Implementation Anchors | Notes | | Criteria Group | Acceptance IDs | Status | Primary Implementation Anchors | Notes |
|---|---|---|---|---| |---|---|---|---|---|
| Read detail and metadata | RD-1, RD-2, RD-7 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py | Dedicated Document detail route renders metadata, read-only system timestamps, and invalid/missing-id states. | | Read detail and metadata | RD-1, RD-2, RD-7 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py | Dedicated Document detail route renders metadata, read-only system timestamps, and invalid/missing-id states. |
| Related sections and empty states | RD-3, RD-4, RD-5, RD-6 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py | Document detail now links to document-scoped Jobs and Sources views, including empty-state guidance and filtered record rendering. | | Related sections and navigation | RD-3, RD-4, RD-5, RD-6 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py | Document detail now shows linked people plus document-scoped Sources and Jobs navigation for the current document. |
| Update entry and validation | UP-1, UP-2, UP-3, UP-4, UP-5, UP-6 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py; tests/services/test_document_service.py | Dedicated edit page includes required-field validation messaging, date parsing rules, and save path routed back to document detail. | | Update entry, validation, and author linkage | UP-1, UP-2, UP-3, UP-4, UP-5, UP-6 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py; tests/services/test_document_service.py | Dedicated edit page includes required-field validation messaging, date parsing rules, and author relationship selection with save path routed back to document detail. |
| Delete controls and guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py; tests/services/test_document_service.py | Dedicated delete page provides permanent-action confirmation, dependency-category blocking, and guarded backend delete behavior. | | Delete controls and guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Implemented | src/transcription/ui/pages/documents_page.py; src/transcription/services/documents.py; tests/ui/test_documents_page.py; tests/services/test_document_service.py | Dedicated delete page provides permanent-action confirmation, dependency-category blocking, and guarded backend delete behavior. |
## Person ## Person
@@ -23,7 +23,7 @@ Status legend:
| Criteria Group | Acceptance IDs | Status | Primary Implementation Anchors | Notes | | Criteria Group | Acceptance IDs | Status | Primary Implementation Anchors | Notes |
|---|---|---|---|---| |---|---|---|---|---|
| Create flow and validation | CR-1, CR-2, CR-3, CR-4, CR-5 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py | Dedicated Person create page with required full_name validation, optional field handling, and success routing to detail. | | Create flow and validation | CR-1, CR-2, CR-3, CR-4, CR-5 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py | Dedicated Person create page with required full_name validation, optional field handling, and success routing to detail. |
| Read detail and linked documents | RD-1, RD-2, RD-3, RD-4 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py; tests/services/test_document_service.py | Person detail route renders metadata, read-only timestamps, linked-document section, and invalid/missing-id states. | | Read detail and linked documents | RD-1, RD-2, RD-3, RD-4 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py; tests/services/test_document_service.py | Person detail route renders metadata, full-name summary, portrait preview when available, linked-document section, and invalid/missing-id states. |
| Update behavior | UP-1, UP-2, UP-3, UP-4, UP-5 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py; tests/services/test_document_service.py | Dedicated Person edit page supports allowed fields, required full_name validation, and save path back to detail. | | Update behavior | UP-1, UP-2, UP-3, UP-4, UP-5 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py; tests/services/test_document_service.py | Dedicated Person edit page supports allowed fields, required full_name validation, and save path back to detail. |
| Delete behavior and guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py; tests/services/test_document_service.py | Dedicated delete page provides permanent-action confirmation, linked-document blocking message, and guarded backend delete behavior. | | Delete behavior and guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Implemented | src/transcription/ui/pages/people_page.py; src/transcription/services/documents.py; tests/ui/test_people_page.py; tests/services/test_document_service.py | Dedicated delete page provides permanent-action confirmation, linked-document blocking message, and guarded backend delete behavior. |
@@ -33,21 +33,21 @@ Status legend:
|---|---|---|---|---| |---|---|---|---|---|
| Create entry and required links | CR-1, CR-2, CR-4, CR-5 | Implemented | src/transcription/services/store.py; src/transcription/ui/pages/jobs_page.py; src/transcription/ui/pages/upload_page.py; tests/services/test_store.py; tests/ui/test_jobs_page.py | Source upload/create is job-create-context only (legacy upload route redirects), with required Document and JobSource linkage enforced. | | Create entry and required links | CR-1, CR-2, CR-4, CR-5 | Implemented | src/transcription/services/store.py; src/transcription/ui/pages/jobs_page.py; src/transcription/ui/pages/upload_page.py; tests/services/test_store.py; tests/ui/test_jobs_page.py | Source upload/create is job-create-context only (legacy upload route redirects), with required Document and JobSource linkage enforced. |
| Ordering and filename policy | CR-3 | Implemented | src/transcription/services/store.py; tests/services/test_store.py; tests/ui/test_jobs_page.py | Multi-file/folder uploads are ordered alphabetically by original filename, helper text is visible, and stored filenames use generated unique-id plus extension. | | Ordering and filename policy | CR-3 | Implemented | src/transcription/services/store.py; tests/services/test_store.py; tests/ui/test_jobs_page.py | Multi-file/folder uploads are ordered alphabetically by original filename, helper text is visible, and stored filenames use generated unique-id plus extension. |
| Read and revision visibility | RD-1, RD-2, RD-3 | Implemented | src/transcription/ui/pages/jobs_page.py; src/transcription/ui/components/job_detail.py; src/transcription/ui/components/transcript.py | Source preview and revision context are available primarily in job detail flow. | | Read and navigation visibility | RD-1, RD-2, RD-3 | Partial | src/transcription/ui/pages/documents_page.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_documents_page.py; tests/ui/test_jobs_page.py | Source list context is now document-scoped and job detail is link-oriented; source preview and revision surfaces are deferred. |
| Revision update behavior | UP-1, UP-2, UP-3, UP-4 | Implemented | src/transcription/services/transcription.py; src/transcription/ui/pages/jobs_page.py; src/transcription/ui/components/transcript.py | Revised text editing and save feedback path exists in job detail revision flow. | | Revision update behavior | UP-1, UP-2, UP-3, UP-4 | Planned | src/transcription/services/transcription.py; tests/services/test_transcription_service.py | Revised text service support remains, but the current UI does not expose the editor. |
| Delete and dependency guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Implemented | src/transcription/ui/pages/jobs_page.py; src/transcription/services/transcription.py; tests/ui/test_jobs_page.py; tests/services/test_transcription_service.py; tests/services/test_v2_crud.py | Job-detail source context now exposes delete entry with confirmation copy and dependency-aware service guardrails for multi-job links. | | Delete and dependency guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Planned | src/transcription/services/transcription.py; tests/services/test_transcription_service.py | Job-detail source delete UI was removed from the current simplified flow; backend guardrails remain for future reinstatement. |
## Job ## Job
| Criteria Group | Acceptance IDs | Status | Primary Implementation Anchors | Notes | | Criteria Group | Acceptance IDs | Status | Primary Implementation Anchors | Notes |
|---|---|---|---|---| |---|---|---|---|---|
| Create entry and required links | CR-1, CR-2, CR-5, CR-6 | Implemented | src/transcription/services/store.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_jobs_page.py; tests/services/test_store.py | Jobs list now has explicit Create entry and `/jobs/new` create flow with Document selection, upload validation, and submit routing to job detail. | | Create entry and required links | CR-1, CR-2, CR-5, CR-6 | Implemented | src/transcription/services/store.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_jobs_page.py; tests/services/test_store.py | Jobs list now has explicit Create entry and `/jobs/new` create flow with Document selection, combined file/folder upload widget, and submit routing to job detail. |
| Source ordering and upload behavior | CR-3 | Implemented | src/transcription/services/store.py; src/transcription/ui/pages/jobs_page.py; tests/services/test_store.py; tests/ui/test_jobs_page.py | Multi-file and folder upload affordances are present, uploads are sorted alphabetically by original filename, and helper guidance is shown in create UI. | | Source ordering and upload behavior | CR-3 | Implemented | src/transcription/services/store.py; src/transcription/ui/pages/jobs_page.py; tests/services/test_store.py; tests/ui/test_jobs_page.py | Multi-file and folder upload are supported through one widget, uploads are sorted alphabetically by original filename, and helper guidance is shown in create UI. |
| Provider/model/prompt visibility | CR-4, RD-4 | Implemented | src/transcription/services/workflows.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_jobs_page.py | Provider/model/prompt fields are visible in create and detail flows when known (with pending fallback labels). | | Provider/model/prompt visibility | CR-4, RD-4 | Implemented | src/transcription/services/workflows.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_jobs_page.py | Provider/model/prompt fields are visible in create and detail flows when known (with pending fallback labels). |
| Jobs list and detail read states | RD-1, RD-2, RD-3, RD-5 | Implemented | src/transcription/ui/pages/jobs_page.py; src/transcription/ui/components/table/jobs.py; tests/ui/test_jobs_page.py | Jobs list, detail route, and invalid/missing id states are present. | | Jobs list and detail read states | RD-1, RD-2, RD-3, RD-5 | Implemented | src/transcription/ui/pages/jobs_page.py; src/transcription/ui/components/table/jobs.py; tests/ui/test_jobs_page.py | Jobs list, detail route, document-scoped navigation, and invalid/missing id states are present. |
| Revision update behavior | UP-1, UP-2, UP-3, UP-4 | Implemented | src/transcription/ui/pages/jobs_page.py; src/transcription/ui/components/transcript.py; src/transcription/services/transcription.py | Revision editing and save feedback exist in job detail source context. | | Revision update behavior | UP-1, UP-2, UP-3, UP-4 | Planned | src/transcription/services/transcription.py; tests/services/test_transcription_service.py | Revision editing service support remains, but the current UI no longer exposes the editor. |
| Lifecycle visibility and retry indicators | UP-5 | Implemented | src/transcription/services/jobs.py; src/transcription/services/workflows.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_jobs_page.py | Job detail now surfaces lifecycle status plus retry/update metadata while lifecycle fields remain system-managed (no direct user edit controls). | | Lifecycle visibility and retry indicators | UP-5 | Implemented | src/transcription/services/jobs.py; src/transcription/services/workflows.py; src/transcription/ui/pages/jobs_page.py; tests/ui/test_jobs_page.py | Job detail now surfaces lifecycle status plus retry/update metadata while lifecycle fields remain system-managed (no direct user edit controls). |
| Delete and dependency guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Implemented | src/transcription/ui/pages/jobs_page.py; src/transcription/services/jobs.py; tests/ui/test_jobs_page.py; tests/services/test_job_service.py | Dedicated Job delete page provides permanent-action confirmation, processing-state blocked-delete messaging, and dependent JobSource cleanup path when deletion is allowed. | | Delete and dependency guardrails | DL-1, DL-2, DL-3, DL-4, DL-5 | Planned | src/transcription/services/jobs.py; tests/services/test_job_service.py | Job delete UI is deferred in the current simplified flow. |
## Quality Gate Coverage ## Quality Gate Coverage
+1 -1
View File
@@ -90,7 +90,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
@app.get("/ui", include_in_schema=False) @app.get("/ui", include_in_schema=False)
async def ui_redirect() -> RedirectResponse: async def ui_redirect() -> RedirectResponse:
return RedirectResponse(url="/ui/jobs", status_code=status.HTTP_307_TEMPORARY_REDIRECT) return RedirectResponse(url="/ui/documents", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@app.get("/healthz") @app.get("/healthz")
def health() -> dict[str, str]: def health() -> dict[str, str]:
+136 -16
View File
@@ -5,9 +5,12 @@ from __future__ import annotations
from datetime import date from datetime import date
from uuid import UUID from uuid import UUID
from fastapi import Request
from nicegui import ui from nicegui import ui
from transcription.db.models import Document from transcription.db.models import Document
from transcription.db.models import DocumentPerson
from transcription.db.models import DocumentPersonRole
from transcription.errors import ErrorCategory from transcription.errors import ErrorCategory
from transcription.services.documents import DocumentDeleteBlockedError from transcription.services.documents import DocumentDeleteBlockedError
from transcription.services.documents import DocumentError from transcription.services.documents import DocumentError
@@ -21,12 +24,98 @@ from ...db.session import SessionFactoryDep
def register_page() -> None: def register_page() -> None:
"""Register documents list and detail routes.""" """Register documents list and detail routes."""
@ui.page("/documents/new")
async def document_create_page(request: Request, session_factory: SessionFactoryDep) -> None:
document_service = DocumentService(session_factory=session_factory)
render_navigation_header(current_path="/documents")
ui.label("Create document").classes("text-h5 text-weight-medium")
ui.label("Document name is required.").classes("text-body2 vibe-text-muted")
name_input = ui.input(label="Document name").props("outlined")
document_type_input = ui.input(label="Document type").props("outlined")
date_input = ui.input(label="Exact date (YYYY-MM-DD)").props('outlined type="date"')
date_raw_input = ui.input(label="Approximate date").props("outlined")
location_input = ui.input(label="Document location").props("outlined")
archive_input = ui.input(label="Archive identifier").props("outlined")
notes_input = ui.textarea(label="Notes").props("outlined autogrow")
people = sorted(await document_service.list_people(), key=lambda item: item.full_name.casefold())
author_options = {"": "No author"} | {str(person.id): person.full_name for person in people}
author_select = ui.select(author_options, label="Author (Person)", value="").props("outlined")
return_to = request.query_params.get("return_to")
async def submit_create() -> None:
candidate_name = (name_input.value or "").strip()
if not candidate_name:
ui.notify("Document name is required.", type="warning")
return
parsed_date: date | None = None
candidate_date_text = (date_input.value or "").strip()
if candidate_date_text:
try:
parsed_date = date.fromisoformat(candidate_date_text)
except ValueError:
ui.notify("Exact date must use YYYY-MM-DD.", type="warning")
return
candidate = Document(
name=candidate_name,
document_type=(document_type_input.value or "").strip() or None,
document_date=parsed_date,
document_date_raw=(date_raw_input.value or "").strip() or None,
location_created=(location_input.value or "").strip() or None,
notes=(notes_input.value or "").strip() or None,
archive_identifier=(archive_input.value or "").strip() or None,
)
try:
created = await document_service.create_document(candidate)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Create failed", operation="documents.create")
return
selected_author = (author_select.value or "").strip()
if selected_author:
try:
parsed_person_id = UUID(selected_author)
except ValueError:
ui.notify("Selected author is invalid.", type="warning")
return
try:
await document_service.create_document_person(
DocumentPerson(
document_id=created.id,
person_id=parsed_person_id,
role=DocumentPersonRole.AUTHOR,
)
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Author link failed", operation="documents.create.link_author")
return
ui.notify("Document created", type="positive")
if return_to == "jobs_new":
ui.navigate.to(f"/jobs/new?document_id={created.id}")
return
ui.navigate.to(f"/documents/{created.id}")
with ui.row().classes("w-full items-center gap-2"):
ui.button("Save document", on_click=submit_create, icon="save").props('unelevated color="primary"')
ui.button("Cancel", on_click=lambda: ui.navigate.to("/documents"), icon="arrow_back")
@ui.page("/documents") @ui.page("/documents")
async def documents_page(session_factory: SessionFactoryDep) -> None: async def documents_page(session_factory: SessionFactoryDep) -> None:
document_service = DocumentService(session_factory=session_factory) document_service = DocumentService(session_factory=session_factory)
render_navigation_header(current_path="/documents") render_navigation_header(current_path="/documents")
ui.label("Documents").classes("text-h5 text-weight-medium") with ui.row().classes("w-full items-center justify-between"):
ui.label("Documents").classes("text-h5 text-weight-medium")
ui.button("Create new document", on_click=lambda: ui.navigate.to("/documents/new"), icon="note_add").props(
'unelevated color="primary"'
)
try: try:
documents = sorted( documents = sorted(
@@ -40,6 +129,9 @@ def register_page() -> None:
if not documents: if not documents:
ui.label("No documents yet.").classes("text-body1 vibe-text-muted") ui.label("No documents yet.").classes("text-body1 vibe-text-muted")
ui.button("Create your first document", on_click=lambda: ui.navigate.to("/documents/new"), icon="note_add").props(
'unelevated color="primary"'
)
return return
with ui.column().classes("w-full gap-2"): with ui.column().classes("w-full gap-2"):
@@ -77,6 +169,13 @@ def register_page() -> None:
ui.label(document.name).classes("text-h5 text-weight-medium") ui.label(document.name).classes("text-h5 text-weight-medium")
ui.label(f"Document type: {document.document_type or 'unspecified'}").classes("text-subtitle1") ui.label(f"Document type: {document.document_type or 'unspecified'}").classes("text-subtitle1")
author_link = next(
(item for item in document.document_people if item.role == DocumentPersonRole.AUTHOR and item.person is not None),
None,
)
ui.label(f"Author: {author_link.person.full_name if author_link and author_link.person is not None else 'not set'}").classes(
"text-body2"
)
with ui.row().classes("w-full items-center gap-2"): with ui.row().classes("w-full items-center gap-2"):
ui.button("Edit document", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/edit"), icon="edit").props( ui.button("Edit document", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/edit"), icon="edit").props(
@@ -111,40 +210,29 @@ def register_page() -> None:
ui.label("Sources").classes("text-subtitle1 text-weight-medium") ui.label("Sources").classes("text-subtitle1 text-weight-medium")
with ui.row().classes("w-full items-center gap-2"): with ui.row().classes("w-full items-center gap-2"):
ui.button( ui.button(
"View sources", "Sources",
on_click=lambda: ui.navigate.to(f"/documents/{document.id}/sources"), on_click=lambda: ui.navigate.to(f"/documents/{document.id}/sources"),
icon="description", icon="description",
).props("flat") ).props("flat")
if not document.sources:
ui.label("No sources added yet.").classes("text-body2 vibe-text-muted")
ui.button( ui.button(
"Add sources", "Add sources",
on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"), on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"),
icon="upload_file", icon="upload_file",
).props('unelevated color="primary"') ).props('unelevated color="primary"')
else: ui.label(f"{len(document.sources)} source(s) linked").classes("text-body2")
for source in sorted(document.sources, key=lambda item: item.page_number):
ui.label(f"Page {source.page_number}: {source.upload_name}").classes("text-body2")
ui.separator() ui.separator()
ui.label("Jobs").classes("text-subtitle1 text-weight-medium") ui.label("Jobs").classes("text-subtitle1 text-weight-medium")
with ui.row().classes("w-full items-center gap-2"): with ui.row().classes("w-full items-center gap-2"):
ui.button("View jobs", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/jobs"), icon="work_history").props( ui.button("Jobs", on_click=lambda: ui.navigate.to(f"/documents/{document.id}/jobs"), icon="work_history").props(
"flat" "flat"
) )
if not document.jobs:
ui.label("No jobs created yet.").classes("text-body2 vibe-text-muted")
ui.button( ui.button(
"Create job", "Create job",
on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"), on_click=lambda: ui.navigate.to(f"/jobs/new?document_id={document.id}"),
icon="add", icon="add",
).props('unelevated color="primary"') ).props('unelevated color="primary"')
else: ui.label(f"{len(document.jobs)} job(s) linked").classes("text-body2")
with ui.column().classes("w-full gap-1"):
for job in sorted(document.jobs, key=lambda item: item.date_created, reverse=True):
with ui.row().classes("w-full items-center justify-between"):
ui.label(f"{job.status.value} - {job.id}").classes("text-body2")
ui.button("Open", on_click=lambda _=None, job_id=job.id: ui.navigate.to(f"/jobs/{job_id}"), icon="open_in_new").props("flat")
@ui.page("/documents/{document_id}/jobs") @ui.page("/documents/{document_id}/jobs")
async def document_jobs_page(document_id: str, session_factory: SessionFactoryDep) -> None: async def document_jobs_page(document_id: str, session_factory: SessionFactoryDep) -> None:
@@ -254,6 +342,14 @@ def register_page() -> None:
location_input = ui.input(label="Document location", value=document.location_created or "").props("outlined") location_input = ui.input(label="Document location", value=document.location_created or "").props("outlined")
archive_input = ui.input(label="Archive identifier", value=document.archive_identifier or "").props("outlined") archive_input = ui.input(label="Archive identifier", value=document.archive_identifier or "").props("outlined")
notes_input = ui.textarea(label="Notes", value=document.notes or "").props("outlined autogrow") notes_input = ui.textarea(label="Notes", value=document.notes or "").props("outlined autogrow")
people = sorted(await document_service.list_people(), key=lambda item: item.full_name.casefold())
author_options = {"": "No author"} | {str(person.id): person.full_name for person in people}
existing_author = next(
(item for item in document.document_people if item.role == DocumentPersonRole.AUTHOR),
None,
)
author_value = str(existing_author.person_id) if existing_author is not None else ""
author_select = ui.select(author_options, label="Author (Person)", value=author_value).props("outlined")
async def submit_edit() -> None: async def submit_edit() -> None:
candidate_name = (name_input.value or "").strip() candidate_name = (name_input.value or "").strip()
@@ -293,6 +389,30 @@ def register_page() -> None:
show_error(exc, title="Save failed", operation="documents.edit.save") show_error(exc, title="Save failed", operation="documents.edit.save")
return return
selected_author = (author_select.value or "").strip()
existing_author_links = [
link for link in document.document_people if link.role == DocumentPersonRole.AUTHOR
]
try:
if not selected_author:
for link in existing_author_links:
await document_service.delete_document_person(link)
else:
selected_author_id = UUID(selected_author)
if not any(link.person_id == selected_author_id for link in existing_author_links):
for link in existing_author_links:
await document_service.delete_document_person(link)
await document_service.create_document_person(
DocumentPerson(
document_id=document.id,
person_id=selected_author_id,
role=DocumentPersonRole.AUTHOR,
)
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Author update failed", operation="documents.edit.link_author")
return
ui.notify("Document updated", type="positive") ui.notify("Document updated", type="positive")
ui.navigate.to(f"/documents/{document.id}") ui.navigate.to(f"/documents/{document.id}")
+40 -143
View File
@@ -8,26 +8,19 @@ from uuid import UUID
from nicegui import ui from nicegui import ui
from transcription.db.models import Job
from transcription.db.models import JobStatus from transcription.db.models import JobStatus
from transcription.db.models import Source
from transcription.db.session import session_scope from transcription.db.session import session_scope
from transcription.services.documents import DocumentService from transcription.services.documents import DocumentService
from transcription.services.jobs import JobDeleteBlockedError from transcription.services.jobs import JobDeleteBlockedError
from transcription.services.jobs import JobService from transcription.services.jobs import JobService
from transcription.services.transcription import SourceDeleteBlockedError
from transcription.services.store import create_job_for_document from transcription.services.store import create_job_for_document
from transcription.services.transcription import TranscriptionService
from transcription.ui.components.app_shell import render_navigation_header from transcription.ui.components.app_shell import render_navigation_header
from transcription.ui.components.error_presenter import show_error from transcription.ui.components.error_presenter import show_error
from transcription.ui.components.table.jobs import render_jobs_table from transcription.ui.components.table.jobs import render_jobs_table
from transcription.worker import resolve_worker_notifier from transcription.worker import resolve_worker_notifier
from ...db.session import SessionFactoryDep from ...db.session import SessionFactoryDep
from ..components.document_panzoom import render_document_panzoom
from ..components.table.jobs import JobTableRow from ..components.table.jobs import JobTableRow
from ..components.transcript import render_original_transcription_card
from ..components.transcript import render_revision_row
def register_page() -> None: # noqa: PLR0915 def register_page() -> None: # noqa: PLR0915
@@ -72,6 +65,11 @@ def register_page() -> None: # noqa: PLR0915
"text-body1 text-warning" "text-body1 text-warning"
) )
with ui.row(): with ui.row():
ui.button(
"Create document",
on_click=lambda: ui.navigate.to("/documents/new?return_to=jobs_new"),
icon="note_add",
).props('unelevated color="primary"')
ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back") ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back")
return return
@@ -131,13 +129,7 @@ def register_page() -> None: # noqa: PLR0915
ui.upload( ui.upload(
on_upload=on_upload, on_upload=on_upload,
auto_upload=True, auto_upload=True,
label="Select source files", label="Select source files or a folder",
).props('accept=".jpg,.jpeg,.png,.tif,.tiff,.pdf" multiple')
ui.upload(
on_upload=on_upload,
auto_upload=True,
label="Upload folder",
).props('accept=".jpg,.jpeg,.png,.tif,.tiff,.pdf" webkitdirectory directory multiple') ).props('accept=".jpg,.jpeg,.png,.tif,.tiff,.pdf" webkitdirectory directory multiple')
render_upload_list() render_upload_list()
@@ -182,9 +174,8 @@ def register_page() -> None: # noqa: PLR0915
ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back") ui.button("Back to Jobs", on_click=lambda: ui.navigate.to("/jobs"), icon="arrow_back")
@ui.page("/jobs/{job_id}") @ui.page("/jobs/{job_id}")
async def job_detail_page(job_id: str, session_factory: SessionFactoryDep) -> None: # noqa: PLR0915 async def job_detail_page(job_id: str, session_factory: SessionFactoryDep) -> None:
jobs_service = JobService(session_factory=session_factory) jobs_service = JobService(session_factory=session_factory)
transcription_service = TranscriptionService(session_factory=session_factory)
render_navigation_header(current_path="/jobs") render_navigation_header(current_path="/jobs")
try: try:
@@ -199,126 +190,42 @@ def register_page() -> None: # noqa: PLR0915
ui.label("Job not found").classes("text-h6 text-negative") ui.label("Job not found").classes("text-h6 text-negative")
return return
source = _resolve_primary_source(job) with ui.column().classes("w-full gap-3"):
with ui.row().classes("w-full items-center justify-between"):
ui.button(icon="arrow_back", on_click=ui.navigate.back)
match job.status:
case JobStatus.TRANSCRIBED:
ui.chip(job.status.value.upper(), color="positive", text_color="white").props("outline")
case _:
ui.label(f"{job.status.value}").classes("text-subtitle1 text-weight-medium")
with ui.splitter(value=30).classes("w-full h-[calc(100vh-64px)]") as splitter: ui.label(f"Job {job.id}").classes("text-h6 text-weight-bold")
with splitter.before, ui.column(align_items="stretch").classes("w-full h-full p-4 gap-3"):
if source is not None:
render_document_panzoom(source=source)
else:
ui.label("No source preview is available for this job.").classes("text-body2 vibe-text-muted")
with splitter.after, ui.column(align_items="stretch").classes("w-full h-full p-4 gap-3"):
with ui.row():
ui.button(icon="arrow_back", on_click=ui.navigate.back)
with ui.row().classes("w-full items-center justify-between"):
ui.label(f"{job.id}").classes("text-h6 text-weight-bold")
match job.status:
case JobStatus.TRANSCRIBED:
ui.chip(job.status.value.upper(), color="positive", text_color="white").props("outline")
case _:
ui.label(f"{job.status.value}").classes("text-subtitle1 text-weight-medium")
with ui.row().classes("w-full items-center gap-2"): with ui.column().classes("gap-1"):
ui.button( ui.label(f"Provider: {job.provider or 'pending'}").classes("text-body2")
"Delete job", ui.label(f"Model: {job.model or 'pending'}").classes("text-body2")
on_click=lambda: ui.navigate.to(f"/jobs/{parsed_job_id}/delete"), ui.label(f"Prompt: {job.prompt_name or 'pending'}").classes("text-body2")
icon="delete", ui.label(f"Retry count: {job.retry_count}").classes("text-body2")
).props("outline color=negative") ui.label(f"Last updated: {job.date_updated.isoformat()}").classes("text-body2")
with ui.column().classes("gap-1"): ui.separator()
ui.label(f"Provider: {job.provider or 'pending'}").classes("text-body2") ui.label("Document Links").classes("text-subtitle1 text-weight-medium")
ui.label(f"Model: {job.model or 'pending'}").classes("text-body2") with ui.row().classes("w-full items-center gap-2"):
ui.label(f"Prompt: {job.prompt_name or 'pending'}").classes("text-body2") ui.button(
ui.label(f"Retry count: {job.retry_count}").classes("text-body2") "Document",
ui.label(f"Last updated: {job.date_updated.isoformat()}").classes("text-body2") on_click=lambda: ui.navigate.to(f"/documents/{job.document_id}"),
icon="description",
render_original_transcription_card(job=job) ).props("flat")
ui.button(
@ui.refreshable "Sources",
async def render_revision_panel() -> None: on_click=lambda: ui.navigate.to(f"/documents/{job.document_id}/sources"),
refreshed_job = await jobs_service.read_job(job_id=parsed_job_id) icon="description",
refreshed_source = _resolve_primary_source(refreshed_job) ).props("flat")
if refreshed_source is None: ui.button(
ui.label("No source is available for revision editing.").classes("text-body2 vibe-text-muted") "Jobs",
return on_click=lambda: ui.navigate.to(f"/documents/{job.document_id}/jobs"),
icon="work_history",
current_revision_text = refreshed_source.revised_text ).props("flat")
default_revision_text = current_revision_text or ""
ui.label("Revision Editor").classes("text-subtitle1 text-weight-medium")
editor = ui.textarea(label="Revision text", value=default_revision_text).props("autogrow outlined")
editor.classes("w-full")
async def delete_source() -> None:
try:
await transcription_service.delete_source_from_job_context(
job_id=parsed_job_id,
source_id=refreshed_source.id,
)
except SourceDeleteBlockedError as exc:
ui.notify(exc.message, type="warning")
return
except Exception as exc: # noqa: BLE001
show_error(exc, title="Delete source failed", operation="jobs.delete_source")
return
ui.notify("Source deleted", type="positive")
ui.navigate.to(f"/jobs/{parsed_job_id}")
async def confirm_delete_source() -> None:
delete_dialog.close()
await delete_source()
async def save_revision() -> None:
candidate = (editor.value or "").strip()
if not candidate:
ui.notify("Revision text is required.", type="warning")
return
try:
await transcription_service.upsert_revision_for_source(
source_id=refreshed_source.id,
text=candidate,
)
except Exception as exc: # noqa: BLE001
show_error(exc, title="Save failed", operation="jobs.save_revision")
return
ui.notify("Revision saved", type="positive")
await render_revision_panel.refresh()
with ui.row().classes("w-full justify-end gap-2"):
with ui.dialog() as delete_dialog, ui.card().classes("min-w-[22rem]"):
ui.label("Delete source").classes("text-subtitle1 text-weight-medium")
ui.label("This permanently deletes the source from this job context.").classes("text-body2")
ui.label("If related job history exists, deletion may be blocked.").classes(
"text-body2 vibe-text-muted"
)
with ui.row().classes("w-full justify-end gap-2"):
ui.button("Cancel", on_click=delete_dialog.close)
ui.button(
"Delete source",
on_click=confirm_delete_source,
icon="delete_forever",
).props('unelevated color="negative"')
ui.button("Delete source", on_click=delete_dialog.open, icon="delete").props("outline color=negative")
ui.button(
"Create revision" if current_revision_text is None else "Update revision",
on_click=save_revision,
icon="save",
).props('unelevated color="primary"')
if current_revision_text is None:
ui.label("No source revision exists for this source.").classes("text-body2 vibe-text-muted")
return
render_revision_row(
revision=refreshed_source,
initially_expanded=True,
)
await render_revision_panel()
@ui.page("/jobs/{job_id}/delete") @ui.page("/jobs/{job_id}/delete")
async def job_delete_page(job_id: str, session_factory: SessionFactoryDep) -> None: async def job_delete_page(job_id: str, session_factory: SessionFactoryDep) -> None:
@@ -375,13 +282,3 @@ def register_page() -> None: # noqa: PLR0915
) )
ui.button("Cancel", on_click=lambda: ui.navigate.to(f"/jobs/{job.id}"), icon="arrow_back") ui.button("Cancel", on_click=lambda: ui.navigate.to(f"/jobs/{job.id}"), icon="arrow_back")
def _resolve_primary_source(job: Job) -> Source | None:
if not job.job_sources:
return None
for job_source in job.job_sources:
if job_source.source is not None:
return job_source.source
return None
+42 -4
View File
@@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import date from datetime import date
from urllib.parse import quote
from uuid import UUID from uuid import UUID
from nicegui import ui from nicegui import ui
@@ -29,6 +30,35 @@ def _parse_optional_date(value: str | None, *, label: str) -> date | None:
raise ValueError(f"{label} must use YYYY-MM-DD.") from exc raise ValueError(f"{label} must use YYYY-MM-DD.") from exc
def _bind_portrait_file_picker(portrait_path_input: ui.input) -> None:
async def on_portrait_selected(event) -> None:
portrait_path_input.value = event.file.name
ui.notify("Portrait filename selected. Edit the path if needed.", type="info")
ui.upload(
on_upload=on_portrait_selected,
auto_upload=True,
label="Choose portrait file",
).props('accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,.tif,.tiff"')
ui.label("This picker fills the filename from your selected image.").classes("text-body2 vibe-text-muted")
def _resolve_portrait_src(path: str | None) -> str | None:
candidate = (path or "").strip()
if not candidate:
return None
normalized = candidate.replace("\\", "/")
lowered = normalized.casefold()
if lowered.startswith("http://") or lowered.startswith("https://") or lowered.startswith("data:"):
return normalized
if normalized.startswith("/"):
return normalized
if lowered.startswith("uploads/"):
return f"/{normalized}"
return f"/uploads/{quote(normalized)}"
def register_page() -> None: # noqa: PLR0915 def register_page() -> None: # noqa: PLR0915
"""Register people list and CRUD routes.""" """Register people list and CRUD routes."""
@@ -82,16 +112,17 @@ def register_page() -> None: # noqa: PLR0915
display_name_input = ui.input(label="Display name").props("outlined") display_name_input = ui.input(label="Display name").props("outlined")
maiden_name_input = ui.input(label="Maiden name").props("outlined") maiden_name_input = ui.input(label="Maiden name").props("outlined")
birth_date_input = ui.input(label="Birth date (YYYY-MM-DD)").props("outlined") birth_date_input = ui.input(label="Birth date (YYYY-MM-DD)").props('outlined type="date"')
birth_date_raw_input = ui.input(label="Birth date (approximate/raw)").props("outlined") birth_date_raw_input = ui.input(label="Birth date (approximate/raw)").props("outlined")
birth_place_input = ui.input(label="Birth place").props("outlined") birth_place_input = ui.input(label="Birth place").props("outlined")
death_date_input = ui.input(label="Death date (YYYY-MM-DD)").props("outlined") death_date_input = ui.input(label="Death date (YYYY-MM-DD)").props('outlined type="date"')
death_date_raw_input = ui.input(label="Death date (approximate/raw)").props("outlined") death_date_raw_input = ui.input(label="Death date (approximate/raw)").props("outlined")
death_place_input = ui.input(label="Death place").props("outlined") death_place_input = ui.input(label="Death place").props("outlined")
biography_input = ui.textarea(label="Biography").props("outlined autogrow") biography_input = ui.textarea(label="Biography").props("outlined autogrow")
portrait_path_input = ui.input(label="Portrait path").props("outlined") portrait_path_input = ui.input(label="Portrait path").props("outlined")
_bind_portrait_file_picker(portrait_path_input)
async def submit_create() -> None: async def submit_create() -> None:
full_name = (full_name_input.value or "").strip() full_name = (full_name_input.value or "").strip()
@@ -166,6 +197,7 @@ def register_page() -> None: # noqa: PLR0915
).props("outline color=negative") ).props("outline color=negative")
with ui.column().classes("w-full gap-1"): with ui.column().classes("w-full gap-1"):
ui.label(f"Full name: {person.full_name}")
ui.label(f"Display name: {person.display_name or 'not set'}") ui.label(f"Display name: {person.display_name or 'not set'}")
ui.label(f"Maiden name: {person.maiden_name or 'not set'}") ui.label(f"Maiden name: {person.maiden_name or 'not set'}")
ui.label(f"Birth date: {person.birth_date.isoformat() if person.birth_date else 'not set'}") ui.label(f"Birth date: {person.birth_date.isoformat() if person.birth_date else 'not set'}")
@@ -176,6 +208,11 @@ def register_page() -> None: # noqa: PLR0915
ui.label(f"Death place: {person.death_place or 'not set'}") ui.label(f"Death place: {person.death_place or 'not set'}")
ui.label(f"Biography: {person.biography or 'not set'}") ui.label(f"Biography: {person.biography or 'not set'}")
ui.label(f"Portrait path: {person.portrait_path or 'not set'}") ui.label(f"Portrait path: {person.portrait_path or 'not set'}")
portrait_src = _resolve_portrait_src(person.portrait_path)
if portrait_src is not None:
ui.image(portrait_src).classes("w-40 rounded shadow")
else:
ui.label("No portrait image set.").classes("text-body2 vibe-text-muted")
ui.label(f"Created at (read-only): {person.created_at.isoformat()}").classes("text-body2") ui.label(f"Created at (read-only): {person.created_at.isoformat()}").classes("text-body2")
ui.label(f"Updated at (read-only): {person.updated_at.isoformat()}").classes("text-body2") ui.label(f"Updated at (read-only): {person.updated_at.isoformat()}").classes("text-body2")
@@ -230,7 +267,7 @@ def register_page() -> None: # noqa: PLR0915
birth_date_input = ui.input( birth_date_input = ui.input(
label="Birth date (YYYY-MM-DD)", label="Birth date (YYYY-MM-DD)",
value=person.birth_date.isoformat() if person.birth_date else "", value=person.birth_date.isoformat() if person.birth_date else "",
).props("outlined") ).props('outlined type="date"')
birth_date_raw_input = ui.input(label="Birth date (approximate/raw)", value=person.birth_date_raw or "").props( birth_date_raw_input = ui.input(label="Birth date (approximate/raw)", value=person.birth_date_raw or "").props(
"outlined" "outlined"
) )
@@ -239,7 +276,7 @@ def register_page() -> None: # noqa: PLR0915
death_date_input = ui.input( death_date_input = ui.input(
label="Death date (YYYY-MM-DD)", label="Death date (YYYY-MM-DD)",
value=person.death_date.isoformat() if person.death_date else "", value=person.death_date.isoformat() if person.death_date else "",
).props("outlined") ).props('outlined type="date"')
death_date_raw_input = ui.input(label="Death date (approximate/raw)", value=person.death_date_raw or "").props( death_date_raw_input = ui.input(label="Death date (approximate/raw)", value=person.death_date_raw or "").props(
"outlined" "outlined"
) )
@@ -247,6 +284,7 @@ def register_page() -> None: # noqa: PLR0915
biography_input = ui.textarea(label="Biography", value=person.biography or "").props("outlined autogrow") biography_input = ui.textarea(label="Biography", value=person.biography or "").props("outlined autogrow")
portrait_path_input = ui.input(label="Portrait path", value=person.portrait_path or "").props("outlined") portrait_path_input = ui.input(label="Portrait path", value=person.portrait_path or "").props("outlined")
_bind_portrait_file_picker(portrait_path_input)
async def submit_edit() -> None: async def submit_edit() -> None:
full_name = (full_name_input.value or "").strip() full_name = (full_name_input.value or "").strip()
+30 -6
View File
@@ -29,7 +29,28 @@ class TestDocumentsPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Documents" in response.text assert "Documents" in response.text
assert "Create new document" in response.text
assert "No documents yet." in response.text assert "No documents yet." in response.text
assert "Create your first document" in response.text
def test_document_create_page_renders_fields(self, app_client):
"""GET /ui/documents/new renders document-create form fields."""
_, client = app_client
response = client.get("/ui/documents/new")
assert response.status_code == 200
assert "Create document" in response.text
assert "Document name is required." in response.text
assert "Document name" in response.text
assert "Document type" in response.text
assert "Author (Person)" in response.text
assert "Exact date (YYYY-MM-DD)" in response.text
assert "Approximate date" in response.text
assert "Document location" in response.text
assert "Archive identifier" in response.text
assert "Notes" in response.text
assert "Save document" in response.text
def test_documents_page_lists_seeded_documents(self, app_client): def test_documents_page_lists_seeded_documents(self, app_client):
"""GET /ui/documents lists seeded document cards.""" """GET /ui/documents lists seeded document cards."""
@@ -75,6 +96,7 @@ class TestDocumentsPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Zenna Letter" in response.text assert "Zenna Letter" in response.text
assert "Document type: letter" in response.text assert "Document type: letter" in response.text
assert "Author: not set" in response.text
assert "Exact date: 1885-07-13" in response.text assert "Exact date: 1885-07-13" in response.text
assert "Approximate date: c. 1885" in response.text assert "Approximate date: c. 1885" in response.text
assert "Location created: Ohio" in response.text assert "Location created: Ohio" in response.text
@@ -83,12 +105,12 @@ class TestDocumentsPageRendering:
assert "Created at (read-only):" in response.text assert "Created at (read-only):" in response.text
assert "Updated at (read-only):" in response.text assert "Updated at (read-only):" in response.text
assert "No linked people yet." in response.text assert "No linked people yet." in response.text
assert "No sources added yet." in response.text assert "0 source(s) linked" in response.text
assert "No jobs created yet." in response.text assert "0 job(s) linked" in response.text
assert "Add sources" in response.text assert "Add sources" in response.text
assert "Create job" in response.text assert "Create job" in response.text
assert "View sources" in response.text assert "Sources" in response.text
assert "View jobs" in response.text assert "Jobs" in response.text
assert "Edit document" in response.text assert "Edit document" in response.text
assert "Delete document" in response.text assert "Delete document" in response.text
@@ -135,8 +157,9 @@ class TestDocumentsPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Jane Doe (author)" in response.text assert "Jane Doe (author)" in response.text
assert "Page 1: 001_page.png" in response.text assert "Author: Jane Doe" in response.text
assert "queued -" in response.text assert "1 source(s) linked" in response.text
assert "1 job(s) linked" in response.text
def test_document_jobs_page_filters_to_document_context(self, app_client): def test_document_jobs_page_filters_to_document_context(self, app_client):
_, client = app_client _, client = app_client
@@ -245,6 +268,7 @@ class TestDocumentsPageRendering:
assert "Document name and document type are required." in response.text assert "Document name and document type are required." in response.text
assert "Document name" in response.text assert "Document name" in response.text
assert "Document type" in response.text assert "Document type" in response.text
assert "Author (Person)" in response.text
assert "Exact date (YYYY-MM-DD)" in response.text assert "Exact date (YYYY-MM-DD)" in response.text
assert "Approximate date" in response.text assert "Approximate date" in response.text
assert "Document location" in response.text assert "Document location" in response.text
+12 -41
View File
@@ -1,7 +1,6 @@
"""Tests for the jobs page route.""" """Tests for the jobs page route."""
import asyncio import asyncio
from pathlib import Path
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
@@ -33,6 +32,7 @@ class TestPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Create job" in response.text assert "Create job" in response.text
assert "No documents available. Create a Document before creating a Job." in response.text assert "No documents available. Create a Document before creating a Job." in response.text
assert "Create document" in response.text
def test_job_create_page_lists_available_documents(self, app_client): def test_job_create_page_lists_available_documents(self, app_client):
"""GET /ui/jobs/new renders document choices when Documents exist.""" """GET /ui/jobs/new renders document choices when Documents exist."""
@@ -52,7 +52,7 @@ class TestPageRendering:
assert "Seeded Document" in response.text assert "Seeded Document" in response.text
assert "Files are processed alphabetically by original filename." in response.text assert "Files are processed alphabetically by original filename." in response.text
assert "No files uploaded yet." in response.text assert "No files uploaded yet." in response.text
assert "Upload folder" in response.text assert "Select source files or a folder" in response.text
def test_jobs_page_lists_seeded_jobs(self, app_client, seed_job): def test_jobs_page_lists_seeded_jobs(self, app_client, seed_job):
"""GET /ui/jobs lists seeded jobs from the in-memory database.""" """GET /ui/jobs lists seeded jobs from the in-memory database."""
@@ -65,37 +65,28 @@ class TestPageRendering:
assert "sample.pdf" in response.text assert "sample.pdf" in response.text
assert "transcribed" in response.text assert "transcribed" in response.text
def test_job_detail_page_renders_seeded_job(self, app_client, seed_job): def test_job_detail_page_renders_document_links(self, app_client, seed_job):
"""GET /ui/jobs/{job_id} renders detail content for a real seeded job.""" """GET /ui/jobs/{job_id} renders document-scoped navigation links."""
_, client = app_client _, client = app_client
fixture_path = Path(__file__).resolve().parents[1] / "fixtures" / "images" / "valid" / "single_page_pdf.pdf"
job_id = seed_job( job_id = seed_job(
filename="detail.pdf", filename="detail.pdf",
status=JobStatus.TRANSCRIBED, status=JobStatus.TRANSCRIBED,
transcription_text="original text", transcription_text="original text",
revision_text="hello",
source_file=fixture_path,
) )
response = client.get(f"/ui/jobs/{job_id}") response = client.get(f"/ui/jobs/{job_id}")
assert response.status_code == 200 assert response.status_code == 200
assert "Original Transcription" in response.text assert "Job" in response.text
assert "detail.pdf" in response.text
assert "Source revision" in response.text
assert "Source revision" in response.text
assert "hello" in response.text
assert "original text" in response.text
assert "Document preview" in response.text
assert "/uploads/detail.pdf" in response.text
assert "Provider:" in response.text assert "Provider:" in response.text
assert "Model:" in response.text assert "Model:" in response.text
assert "Prompt:" in response.text assert "Prompt:" in response.text
assert "Retry count:" in response.text assert "Retry count:" in response.text
assert "Last updated:" in response.text assert "Last updated:" in response.text
assert "Delete job" in response.text assert "Document Links" in response.text
assert "Delete source" in response.text assert "Sources" in response.text
assert "This permanently deletes the source from this job context." in response.text assert "Jobs" in response.text
assert "Delete job" not in response.text
def test_job_detail_page_rejects_invalid_id(self, app_client): def test_job_detail_page_rejects_invalid_id(self, app_client):
"""GET /ui/jobs/{job_id} shows validation feedback for malformed IDs.""" """GET /ui/jobs/{job_id} shows validation feedback for malformed IDs."""
@@ -114,39 +105,19 @@ class TestPageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Job not found" in response.text assert "Job not found" in response.text
def test_job_detail_page_shows_revision_editor_when_none_exists(self, app_client, seed_job): def test_job_detail_page_hides_delete_action(self, app_client, seed_job):
"""GET /ui/jobs/{job_id} renders revision editor and create action for sources with no revision.""" """GET /ui/jobs/{job_id} does not expose job deletion controls in this revision."""
_, client = app_client _, client = app_client
job_id = seed_job( job_id = seed_job(
filename="no-revision.pdf", filename="no-revision.pdf",
status=JobStatus.TRANSCRIBED, status=JobStatus.TRANSCRIBED,
transcription_text="original text", transcription_text="original text",
revision_text=None,
) )
response = client.get(f"/ui/jobs/{job_id}") response = client.get(f"/ui/jobs/{job_id}")
assert response.status_code == 200 assert response.status_code == 200
assert "Revision Editor" in response.text assert "Delete job" not in response.text
assert "Create revision" in response.text
assert "No source revision exists for this source." in response.text
def test_job_detail_page_shows_update_action_for_existing_revision(self, app_client, seed_job):
"""GET /ui/jobs/{job_id} renders revision editor with update action when revision exists."""
_, client = app_client
job_id = seed_job(
filename="with-revision.pdf",
status=JobStatus.TRANSCRIBED,
transcription_text="original text",
revision_text="hello",
)
response = client.get(f"/ui/jobs/{job_id}")
assert response.status_code == 200
assert "Revision Editor" in response.text
assert "Update revision" in response.text
assert "hello" in response.text
def test_job_delete_page_shows_confirmation_when_not_processing(self, app_client, seed_job): def test_job_delete_page_shows_confirmation_when_not_processing(self, app_client, seed_job):
_, client = app_client _, client = app_client
+1
View File
@@ -85,6 +85,7 @@ class TestPeoplePageRendering:
assert response.status_code == 200 assert response.status_code == 200
assert "Grace Hopper" in response.text assert "Grace Hopper" in response.text
assert "Full name: Grace Hopper" in response.text
assert "Display name: Grace" in response.text assert "Display name: Grace" in response.text
assert "Maiden name: Murray" in response.text assert "Maiden name: Murray" in response.text
assert "Birth date: 1906-12-09" in response.text assert "Birth date: 1906-12-09" in response.text
+3 -3
View File
@@ -15,13 +15,13 @@ class TestPageRendering:
assert response.status_code == 307 assert response.status_code == 307
assert response.headers["location"] == "/ui" assert response.headers["location"] == "/ui"
def test_ui_redirects_to_upload(self, app_client): def test_ui_redirects_to_documents(self, app_client):
"""GET /ui redirects to the jobs page.""" """GET /ui redirects to the documents page."""
_, client = app_client _, client = app_client
response = client.get("/ui", follow_redirects=False) response = client.get("/ui", follow_redirects=False)
assert response.status_code == 307 assert response.status_code == 307
assert response.headers["location"] == "/ui/jobs" assert response.headers["location"] == "/ui/documents"
def test_upload_page_renders_expected_controls(self, app_client): def test_upload_page_renders_expected_controls(self, app_client):
"""GET /ui/upload redirects to the job-create flow.""" """GET /ui/upload redirects to the job-create flow."""