generated from john/python-template
UI updates, changes sync'd to UI docs
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user