Skip to content

UI Reference

The UI package defines NiceGUI pages, widgets, dialogs, and page-level interaction handlers.

Package entrypoint

Purpose: UI package export surface.

Accessibility Project Manager UI package.

Admin page

Purpose: system-level controls and operational administration UI.

Admin panel — manage material categories, workflow steps, printers, embossers, metadata options, and database backups.

Changes applied (see fix_specs.json): FIX-013 Backups tab added: shows scheduler status, recent backup log, and a manual "Run Backup Now" button. FIX-017 Metadata key backfill now shows a dry-run preview dialog before executing, listing proposed key mappings and skipped keys.

admin_page(content_area)

Render the admin settings page.

Source code in accessibility_mgr/ui/admin.py
def admin_page(content_area: ui.element) -> None:
    """Render the admin settings page."""
    content_area.clear()
    with content_area:
        section_header(
            "Admin Settings",
            "Manage material categories, metadata options, workflow steps, "
            "printers, embossers, and database backups",
        )

        with ui.tabs().classes("w-full") as tabs:
            tab_cats     = ui.tab("Material Categories")
            tab_steps    = ui.tab("Workflow Steps")
            tab_printers = ui.tab("Printers")
            tab_emboss   = ui.tab("Embossers")
            tab_meta     = ui.tab("Metadata Options")
            tab_backups  = ui.tab("Backups")   # FIX-013

        with ui.tab_panels(tabs, value=tab_cats).classes("w-full mt-4"):
            with ui.tab_panel(tab_cats):
                section_labels = [s[1] for s in SECTIONS]
                section_keys   = [s[0] for s in SECTIONS]
                sel = ui.select(
                    section_labels, value=section_labels[0], label="Category Section"
                ).classes("w-72 mb-4")
                cat_container = ui.column().classes("w-full")
                _category_section(section_keys[0], section_labels[0], cat_container)

                def _on_section_change(e: object) -> None:
                    try:
                        idx = section_labels.index(sel.value)
                    except ValueError:
                        return
                    _category_section(section_keys[idx], section_labels[idx], cat_container)

                sel.on("update:model-value", _on_section_change)

            with ui.tab_panel(tab_steps):
                step_container = ui.column().classes("w-full")
                _workflow_steps_section(step_container)

            with ui.tab_panel(tab_printers):
                printer_container = ui.column().classes("w-full")
                _printers_section(printer_container)

            with ui.tab_panel(tab_emboss):
                embosser_container = ui.column().classes("w-full")
                _embossers_section(embosser_container)

            with ui.tab_panel(tab_meta):
                metadata_container = ui.column().classes("w-full")
                _metadata_options_section(metadata_container)

            with ui.tab_panel(tab_backups):   # FIX-013
                backup_container = ui.column().classes("w-full")
                _backup_section(backup_container)

Binary integrations dashboard

Purpose: external binary integration status and controls.

Production accessibility binary integration dashboard.

binary_integrations_dashboard(content_area)

Render production binary integration dashboard.

Source code in accessibility_mgr/ui/binary_integrations_dashboard.py
def binary_integrations_dashboard(content_area: ui.element) -> None:
    """Render production binary integration dashboard."""

    content_area.clear()

    ace_binary = _service.discover_binary("ace")
    epubcheck_binary = _service.discover_binary("epubcheck")
    liblouis_binary = (
        _service.discover_binary("file2brl")
        or _service.discover_binary("lou_translate")
    )
    glow_binary = _service.discover_binary("acb-large-print")

    with content_area:
        section_header(
            "Production Accessibility Toolchain",
            "Production DAISY Ace, EPUBCheck, Liblouis, and GLOW (ACB Large Print) binary integrations",
        )

        with ui.grid(columns=4).classes("w-full gap-4"):
            for label, binary in [
                ("DAISY Ace CLI", ace_binary),
                ("EPUBCheck", epubcheck_binary),
                ("Liblouis (file2brl / lou_translate)", liblouis_binary),
                ("GLOW (ACB Large Print)", glow_binary),
            ]:
                with ui.card().classes(
                    "p-5 rounded-xl border border-slate-200"
                ):
                    ui.label(label).classes(
                        "text-lg font-semibold text-slate-700 mb-2"
                    )

                    if binary:
                        ui.badge("installed").classes(
                            "bg-green-100 text-green-700 mb-2"
                        )

                        ui.label(binary).classes(
                            "text-xs font-mono text-slate-500"
                        )
                    else:
                        ui.badge("missing").classes(
                            "bg-red-100 text-red-700 mb-2"
                        )

                        ui.label(
                            "Binary not available in execution environment"
                        ).classes("text-xs text-slate-500")

        with ui.card().classes(
            "w-full mt-6 p-5 rounded-xl border border-slate-200"
        ):
            ui.label("Execution Framework Capabilities").classes(
                "text-base font-semibold text-slate-700 mb-3"
            )

            readiness = [
                "CLI binary discovery",
                "Structured subprocess execution",
                "Artifact output capture",
                "Execution timeout enforcement",
                "Operational telemetry",
            ]

            for item in readiness:
                with ui.row().classes("items-center gap-2 py-1"):
                    ui.icon("info").classes(
                        "text-blue-500"
                    )
                    ui.label(item).classes(
                        "text-sm text-slate-700"
                    )

Braille jobs page

Purpose: braille job listing, creation, and workflow-state updates.

Braille Jobs panel — full workflow tracking with file ingestion, Dublin Core metadata, PREMIS event log, and step management.

Changes applied (see fix_specs.json): FIX-003 _save_all in metadata dialog now calls Q.log_event (persisted to DB). FIX-008 _ingest_dialog pre-populates project context from job metadata so files land in artifacts// not job_files/.. FIX-016 Delivered step opens delivery confirmation dialog instead of direct toggle.

CICD dashboard

Purpose: CI/CD visibility and hook/operation status views.

CI/CD accessibility validation dashboard.

AUDIT-FIX (follow-up): this page used to be a static capability checklist with no way to actually trigger a validation run — it only displayed history that nothing in the app ever wrote to. It now has a real "Run Validation" form that calls CICDValidationHookService against a real EPUB path and persists the result to the database (see integrations/cicd_hooks.py + db/queries.py).

cicd_dashboard(content_area)

Render CI/CD accessibility validation dashboard.

Source code in accessibility_mgr/ui/cicd_dashboard.py
def cicd_dashboard(content_area: ui.element) -> None:
    """Render CI/CD accessibility validation dashboard."""

    content_area.clear()

    with content_area:
        section_header(
            "CI/CD Accessibility Validation",
            "Run DAISY Ace and EPUBCheck as a release gate, and review validation history",
        )

        result_area = ui.column().classes("w-full gap-3 mt-2")

        with ui.card().classes(
            "w-full p-5 rounded-xl border border-slate-200 mb-6"
        ):
            ui.label("Run Validation").classes(
                "text-base font-semibold text-slate-700 mb-3"
            )
            ui.label(
                "Fails the gate if either tool reports violations "
                "(non-zero exit code); warns if a tool isn't installed."
            ).classes("text-xs text-slate-500 mb-3")

            with ui.row().classes("gap-3 w-full items-end"):
                pipeline_id_inp = ui.input(
                    "Pipeline ID",
                    placeholder="e.g. ci-build-1423",
                ).classes("flex-1")
                epub_path_inp = ui.input(
                    "EPUB File Path*",
                    placeholder="/path/to/file.epub",
                ).classes("flex-1")

                def _run_validation() -> None:
                    epub_path = epub_path_inp.value.strip()
                    if not epub_path:
                        notify_error("Enter an EPUB file path first.")
                        return

                    pipeline_id = pipeline_id_inp.value.strip() or "manual-run"

                    result_area.clear()
                    with result_area:
                        with ui.card().classes(
                            "p-4 rounded-xl border border-slate-200 w-full"
                        ):
                            ui.label(
                                f"Validating {epub_path}…"
                            ).classes("text-slate-600 font-medium")
                            ui.spinner("dots", size="sm")

                    def _do() -> None:
                        result = _service.validate_epub_pipeline(
                            pipeline_id=pipeline_id, epub_path=epub_path
                        )
                        result_area.clear()
                        with result_area:
                            _render_validation_result(result)
                            _render_history(_service.list_history())

                    threading.Thread(target=_do, daemon=True).start()

                ui.button("▶ Run Validation", on_click=_run_validation).classes(
                    "bg-blue-600 text-white"
                )

        ui.label("Validation History").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mb-2"
        )
        _render_history(_service.list_history())

Shared UI components

Purpose: reusable badges, dialogs, progress, and notification helpers.

Shared UI helpers — progress bars, badges, confirmation dialogs, notifications.

card_row(*labels, cls='')

Render a row of label-value pairs inside a card layout.

Source code in accessibility_mgr/ui/components.py
def card_row(*labels: tuple[str, Any], cls: str = "") -> None:
    """Render a row of label-value pairs inside a card layout."""
    with ui.row().classes(f"gap-6 flex-wrap {cls}"):
        for key, val in labels:
            with ui.column().classes("gap-0"):
                ui.label(key).classes(
                    "text-xs text-slate-400 uppercase tracking-wider"
                )
                ui.label(str(val) if val is not None else "—").classes(
                    "text-sm text-slate-700 font-medium"
                )

confirm_dialog(message, on_confirm, title='Confirm')

Show a modal confirmation dialog; call on_confirm only if the user confirms.

Source code in accessibility_mgr/ui/components.py
def confirm_dialog(
    message: str,
    on_confirm: Callable[[], None],
    title: str = "Confirm",
) -> None:
    """Show a modal confirmation dialog; call on_confirm only if the user confirms."""
    with ui.dialog() as dialog, ui.card().classes("p-6 gap-4 min-w-80"):
        ui.label(title).classes("text-lg font-semibold text-slate-800")
        ui.label(message).classes("text-slate-600")
        with ui.row().classes("gap-3 justify-end w-full mt-2"):
            ui.button("Cancel", on_click=dialog.close).props("flat").classes(
                "text-slate-600"
            )

            def _do() -> None:
                dialog.close()
                on_confirm()

            ui.button("Confirm", on_click=_do).classes("bg-red-500 text-white")
    dialog.open()

file_picker(holder, *, accept='', hint='Click to select a file from this machine', label='Attach File')

Render a file picker that stages the selected file into FILES_DIR.

Clicking the control opens the native browser file dialog. The chosen file is written into the app staging directory (Q.FILES_DIR) so it satisfies ingest_file's SEC-004 staging requirement, and the staged absolute source path is recorded on holder under source_path (and file_name for the original basename). Callers read holder.get("source_path") from their save handler and pass it as the ingest/copy source. Selecting a file replaces any previously staged one.

SEC-005: the incoming event.name originates in the browser and may carry path separators or traversal sequences, so only a sanitised basename is used to build the staged path.

Source code in accessibility_mgr/ui/components.py
def file_picker(
    holder: dict[str, Any],
    *,
    accept: str = "",
    hint: str = "Click to select a file from this machine",
    label: str = "Attach File",
) -> None:
    """Render a file picker that stages the selected file into FILES_DIR.

    Clicking the control opens the native browser file dialog. The chosen file
    is written into the app staging directory (``Q.FILES_DIR``) so it satisfies
    ``ingest_file``'s SEC-004 staging requirement, and the staged absolute
    source path is recorded on ``holder`` under ``source_path`` (and
    ``file_name`` for the original basename). Callers read
    ``holder.get("source_path")`` from their save handler and pass it as the
    ingest/copy source. Selecting a file replaces any previously staged one.

    SEC-005: the incoming ``event.name`` originates in the browser and may carry
    path separators or traversal sequences, so only a sanitised basename is used
    to build the staged path.
    """
    from ..db import queries as Q

    holder["source_path"] = None
    holder["file_name"] = None

    def _cleanup_previous() -> None:
        prev = holder.get("source_path")
        if prev:
            with contextlib.suppress(OSError):
                Path(prev).unlink(missing_ok=True)

    def _on_upload(event: events.UploadEventArguments) -> None:
        raw_name = Path(event.name).name
        safe_name = "".join(c for c in raw_name if c.isalnum() or c in "._- ").strip()
        if not safe_name:
            notify_error(f"Upload rejected: filename '{event.name}' is not safe.")
            return
        Q.FILES_DIR.mkdir(parents=True, exist_ok=True)
        stage = Q.FILES_DIR / safe_name
        if stage.exists():
            stage = Q.FILES_DIR / (
                f"{Path(safe_name).stem}_{uuid4().hex[:8]}{Path(safe_name).suffix}"
            )
        _cleanup_previous()
        stage.write_bytes(event.content.read())
        holder["source_path"] = str(stage)
        holder["file_name"] = safe_name
        status_label.set_text(f"Attached: {safe_name}")
        notify_success(f"Selected: {safe_name}")

    with ui.column().classes("gap-1 w-full"):
        picker_row = ui.row().classes("w-full items-center gap-2")

        def _open_picker() -> None:
            # Click the hidden upload's file-select input to open the native dialog.
            ui.run_javascript(
                "const el = document.querySelector('.upload-file-picker .q-uploader__input');"
                " if (el) el.click();"
            )

        with picker_row:
            ui.button(label, icon="folder_open", on_click=_open_picker).classes(
                "text-indigo-600 border border-indigo-200 rounded-lg shrink-0"
            )
            with ui.upload(on_upload=_on_upload, auto_upload=True).classes(
                "hidden upload-file-picker"
            ).props(f"accept={accept}" if accept else ""):
                pass

    status_label = ui.label(hint).classes("text-xs text-slate-400")

file_use_badge(file_use)

Render a colored badge indicating the file's role (e.g. ORIGINAL, DERIVATIVE).

Source code in accessibility_mgr/ui/components.py
def file_use_badge(file_use: str) -> None:
    """Render a colored badge indicating the file's role (e.g. ORIGINAL, DERIVATIVE)."""
    cls = FILE_USE_COLORS.get(file_use, "bg-slate-100 text-slate-600")
    ui.badge(file_use).classes(f"text-xs px-2 py-0.5 rounded {cls}")

notify_error(msg)

Show a red error notification toast.

Source code in accessibility_mgr/ui/components.py
def notify_error(msg: str) -> None:
    """Show a red error notification toast."""
    ui.notify(msg, type="negative", position="top-right")

open_folder(path)

Open a folder in the OS file manager. Returns True if launched.

Uses xdg-open on Linux, open on macOS, and explorer on Windows. Because APM runs as a local web app this opens the folder on the host machine in the default file browser.

Source code in accessibility_mgr/ui/components.py
def open_folder(path: str | Path) -> bool:
    """Open a folder in the OS file manager. Returns True if launched.

    Uses ``xdg-open`` on Linux, ``open`` on macOS, and ``explorer`` on Windows.
    Because APM runs as a local web app this opens the folder on the host
    machine in the default file browser.
    """
    target = str(path)
    if not Path(target).exists():
        return False
    try:
        if os.name == "nt":
            subprocess.Popen(["explorer", target])
        elif sys.platform == "darwin":
            subprocess.Popen(["open", target])
        else:
            subprocess.Popen(["xdg-open", target])
        return True
    except OSError:
        return False

progress_bar(done, total)

Render a compact labelled progress bar.

Source code in accessibility_mgr/ui/components.py
def progress_bar(done: int, total: int) -> None:
    """Render a compact labelled progress bar."""
    pct = int(done / total * 100) if total else 0
    with ui.column().classes("gap-0 w-full"):
        with ui.row().classes("w-full items-center gap-2"):
            with ui.element("div").classes("flex-1 bg-slate-200 rounded-full h-2"):
                ui.element("div").classes(
                    f"h-2 rounded-full {'bg-green-500' if pct == 100 else 'bg-blue-500'}"
                ).style(f"width:{pct}%")
            ui.label(f"{done}/{total}").classes("text-xs text-slate-500 whitespace-nowrap")

validate_iso_date(value, label)

Validate YYYY-MM-DD date strings and notify on failure.

Source code in accessibility_mgr/ui/components.py
def validate_iso_date(value: str, label: str) -> bool:
    """Validate YYYY-MM-DD date strings and notify on failure."""
    try:
        date.fromisoformat(value)
        return True
    except ValueError:
        notify_error(f"{label} must be in YYYY-MM-DD format")
        return False

Dashboard page

Purpose: top-level operational summary and quick navigation.

Dashboard — operational overview of the accessibility studio.

Delivery dialog

Purpose: job delivery confirmation and delivery metadata capture.

Shared delivery dialog helper (FIX-016).

Call open_delivery_dialog() from any job detail view when the operator clicks "Mark Done" on the Delivered step. Captures delivery method, recipient, and date before writing the step completion and delivery fields.

open_delivery_dialog(job_type, job_id, on_done, agent='user')

Open a delivery confirmation dialog for any job type.

On confirmation, calls Q.record_delivery() which: - Sets delivered = 1 and all four delivery_ columns - Logs a DELIVERY event to metadata_event - Logs a FIELD_UPDATE event (via update_*_job)

Parameters

job_type : str One of 'braille', 'lp_ebraille', 'tactile', 'print'. job_id : int Primary key of the job being delivered. on_done : callable Zero-argument callback invoked after the DB writes complete. agent : str Name/identifier of the actor confirming delivery.

Source code in accessibility_mgr/ui/delivery_dialog.py
def open_delivery_dialog(
    job_type: str,
    job_id: int,
    on_done,
    agent: str = "user",
) -> None:
    """Open a delivery confirmation dialog for any job type.

    On confirmation, calls Q.record_delivery() which:
      - Sets delivered = 1 and all four delivery_ columns
      - Logs a DELIVERY event to metadata_event
      - Logs a FIELD_UPDATE event (via update_*_job)

    Parameters
    ----------
    job_type : str
        One of 'braille', 'lp_ebraille', 'tactile', 'print'.
    job_id : int
        Primary key of the job being delivered.
    on_done : callable
        Zero-argument callback invoked after the DB writes complete.
    agent : str
        Name/identifier of the actor confirming delivery.
    """
    today = date.today().isoformat()

    # Guard: if the job is already delivered, show a read-only summary.
    _fetch_fn = {
        "braille":     Q.get_braille_job,
        "lp_ebraille": Q.get_lp_job,
        "tactile":     Q.get_tactile_job,
        "print":       Q.get_print_job,
    }.get(job_type)
    if _fetch_fn:
        _row = _fetch_fn(job_id)
        if _row and int(_row.get("delivered") or 0) == 1:
            with ui.dialog() as _info_dlg, ui.card().classes("p-6 gap-4 w-[440px] max-w-full"):
                ui.label("Already Delivered").classes("text-xl font-bold text-slate-800")
                ui.label(
                    f"This job was delivered on {_row.get('delivery_date') or '—'} "
                    f"via {_row.get('delivery_method') or '—'} "
                    f"to {_row.get('delivery_recipient') or '—'}."
                ).classes("text-sm text-slate-600")
                if _row.get("delivery_notes"):
                    ui.label(_row["delivery_notes"]).classes("text-xs text-slate-400")
                ui.button("Close", on_click=_info_dlg.close).classes("bg-slate-200")
            _info_dlg.open()
            return

    with ui.dialog() as dlg, ui.card().classes("p-6 gap-4 w-[480px] max-w-full"):
        ui.label("Confirm Delivery").classes("text-xl font-bold text-slate-800")
        ui.label(
            "Recording delivery will mark this job as delivered and log a permanent audit event."
        ).classes("text-sm text-slate-500 mb-2")

        method_sel = ui.select(
            _DELIVERY_METHODS,
            label="Delivery Method*",
            value="Physical Copy",
        ).classes("w-full")

        recipient_inp = ui.input(
            "Delivered To* (student name, teacher, or org)",
            placeholder="e.g. Smith, John — Legacy Jr. High",
        ).classes("w-full")

        date_inp = ui.input(
            "Delivery Date*",
            value=today,
            placeholder="YYYY-MM-DD",
        ).classes("w-full")

        notes_inp = ui.textarea(
            "Delivery Notes (optional)",
            placeholder="e.g. Delivered to homeroom teacher. Confirmation email sent.",
        ).classes("w-full").props("rows=2")

        with ui.row().classes("justify-end gap-3 mt-2"):
            ui.button("Cancel", on_click=dlg.close).props("flat").classes("text-slate-500")

            def _confirm() -> None:
                if not recipient_inp.value.strip():
                    ui.notify("Recipient is required", type="negative", position="top-right")
                    return
                if not date_inp.value.strip():
                    ui.notify("Delivery date is required", type="negative", position="top-right")
                    return
                if not validate_iso_date(date_inp.value.strip(), "Delivery Date"):
                    return

                Q.record_delivery(
                    job_type=job_type,
                    job_id=job_id,
                    delivery_method=method_sel.value,
                    delivery_recipient=recipient_inp.value.strip(),
                    delivery_date=date_inp.value.strip(),
                    delivery_notes=notes_inp.value.strip(),
                    agent=agent,
                )
                notify_success(
                    f"Delivered to {recipient_inp.value.strip()} "
                    f"via {method_sel.value} on {date_inp.value.strip()}"
                )
                dlg.close()
                on_done()

            ui.button("Confirm Delivery", on_click=_confirm).classes(
                "bg-green-600 text-white"
            )

    dlg.open()

Ingestion page

Purpose: file ingestion workflows and metadata attachment UI.

File Ingestion page — preservation-aware ingest of accessibility-production assets.

Computes SHA-256, stores in job_files/, creates file_object record, and logs a PREMIS INGEST event.

ingestion_page(content_area)

Render the file ingestion page.

Source code in accessibility_mgr/ui/ingestion.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
def ingestion_page(content_area: ui.element) -> None:
    """Render the file ingestion page."""
    content_area.clear()
    with content_area:
        section_header(
            "File Ingestion",
            "Ingest accessibility-production assets with preservation metadata",
        )

        with ui.row().classes("gap-6 flex-wrap items-start w-full"):

            # ── Upload / path form ────────────────────────────────────────────
            with ui.card().classes("flex-1 min-w-80 p-5 rounded-xl border border-slate-200"):
                ui.label("Ingest by File Path").classes(
                    "font-semibold text-slate-700 mb-3"
                )
                ui.label(
                    "Enter the full path to a file already on this machine."
                ).classes("text-sm text-slate-400 mb-3")

                path_inp = ui.input(
                    "File Path*", placeholder="/path/to/document.brf"
                ).classes("w-full")
                fmt_sel = ui.select(
                    SUPPORTED_TYPES, label="Format*", value="BRF"
                ).classes("w-full")
                use_sel = ui.select(
                    FILE_USES, label="File Use*", value="ORIGINAL"
                ).classes("w-full")
                enc_inp = ui.input(
                    "Encoding / Code Table",
                    placeholder="e.g. UEB, Nemeth, EBAE",
                ).classes("w-full")
                ver_inp = ui.input(
                    "Format Version", placeholder="e.g. 2.0"
                ).classes("w-full")

                # ── Artifact / project metadata ───────────────────────────────
                ui.separator().classes("my-3")
                ui.label("Artifact Location (required)").classes(
                    "text-xs font-semibold text-slate-500 uppercase tracking-wider"
                )
                ui.label(
                    "The file will be copied to artifacts/<Project Title>/ and named "
                    "using the fields below (e.g. FrAn_LegacyJr_Grade7_Science.brf)."
                ).classes("text-xs text-slate-400 mb-1")
                project_title_inp = ui.input(
                    "Project Title*", placeholder="e.g. Spring 2026 Braille Production"
                ).classes("w-full")
                student_inp = ui.input(
                    "Student Initials",
                    placeholder="e.g. FrAn  (First two of first + first two of last)",
                ).classes("w-full")
                school_inp = ui.input(
                    "School Name",
                    placeholder="e.g. LegacyJr  (abbreviated, no spaces)",
                ).classes("w-full")
                grade_inp = ui.input(
                    "Grade Level",
                    placeholder="e.g. 7",
                ).classes("w-full")
                subject_inp = ui.input(
                    "Subject",
                    placeholder="e.g. Science",
                ).classes("w-full")
                ui.separator().classes("my-3")
                ui.label("Tools & Processes").classes(
                    "text-xs font-semibold text-slate-500 uppercase tracking-wider"
                )
                ui.label(
                    'Record the tool(s) and process(es) applied at this step — '
                    'e.g. tool: "brailleblaster", process: "digitize".  '
                    'Multiple tools and processes are allowed.'
                ).classes("text-xs text-slate-400 mb-1")

                tool_rows: list[dict] = []
                proc_rows: list[dict] = []

                ui.label("Tools").classes("text-xs font-medium text-slate-500 mt-1")
                tool_col = ui.column().classes("w-full gap-1")

                def _add_tool() -> None:
                    """Add a new tool input row to the tool list."""
                    with tool_col:
                        with ui.row().classes("w-full gap-1 items-center") as row:
                            inp = ui.input(placeholder='e.g. brailleblaster').classes(
                                "flex-1 text-sm"
                            )
                            ref: dict = {"inp": inp, "row": row}
                            tool_rows.append(ref)
                            def _rm_tool(r=ref) -> None:
                                """Remove a tool input row from the tool list."""
                                r["row"].delete()
                                if r in tool_rows:
                                    tool_rows.remove(r)
                            ui.button(icon="close", on_click=_rm_tool).props(
                                "flat dense round size=xs"
                            ).classes("text-slate-400")

                ui.button("+ Add Tool", on_click=_add_tool).props("flat dense").classes(
                    "text-xs text-indigo-600 self-start mt-1"
                )
                _add_tool()

                ui.label("Processes").classes("text-xs font-medium text-slate-500 mt-2")
                proc_col = ui.column().classes("w-full gap-1")

                def _add_proc() -> None:
                    """Add a new process input row to the process list."""
                    with proc_col:
                        with ui.row().classes("w-full gap-1 items-center") as row:
                            inp = ui.input(
                                placeholder='e.g. digitize  (one process per row)'
                            ).classes("flex-1 text-sm")
                            ref = {"inp": inp, "row": row}
                            proc_rows.append(ref)
                            def _rm_proc(r=ref) -> None:
                                """Remove a process input row from the process list."""
                                r["row"].delete()
                                if r in proc_rows:
                                    proc_rows.remove(r)
                            ui.button(icon="close", on_click=_rm_proc).props(
                                "flat dense round size=xs"
                            ).classes("text-slate-400")

                ui.button("+ Add Process", on_click=_add_proc).props("flat dense").classes(
                    "text-xs text-indigo-600 self-start mt-1"
                )
                _add_proc()

                ui.separator().classes("my-3")
                ui.label("Link to Job (optional)").classes(
                    "text-xs font-semibold text-slate-500 uppercase tracking-wider mt-2"
                )
                job_type_sel = ui.select(
                    ["(none)", "braille", "lp_ebraille", "print"],
                    label="Job Type",
                    value="(none)",
                ).classes("w-full")
                job_id_inp = ui.input(
                    "Job ID", placeholder="numeric ID"
                ).classes("w-full")

                result_card: list[ui.element] = []

                def _ingest() -> None:
                    """Validate inputs and ingest a file from a specified path into the database."""
                    src = path_inp.value.strip()
                    if not src:
                        notify_error("File path is required")
                        return
                    if not Path(src).exists():
                        notify_error(f"File not found: {src}")
                        return
                    if not project_title_inp.value.strip():
                        notify_error("Project Title is required for artifact storage")
                        return

                    for el in result_card:
                        el.delete()
                    result_card.clear()

                    try:
                        _tools = [r["inp"].value.strip() for r in tool_rows if r["inp"].value.strip()]
                        _procs = [r["inp"].value.strip() for r in proc_rows if r["inp"].value.strip()]
                        _extra: dict = {}
                        if _tools:
                            _extra["tools"] = _tools
                        if _procs:
                            _extra["processes"] = _procs

                        file_id = Q.ingest_file(
                            source_path=src,
                            file_use=use_sel.value,
                            format_name=fmt_sel.value,
                            format_version=ver_inp.value.strip(),
                            encoding=enc_inp.value.strip(),
                            project_title=project_title_inp.value.strip(),
                            student_initials=student_inp.value.strip(),
                            school_name=school_inp.value.strip(),
                            grade_level=grade_inp.value.strip(),
                            subject=subject_inp.value.strip(),
                            extra_metadata=_extra or None,
                        )

                        jtype = job_type_sel.value
                        jid_raw = job_id_inp.value.strip()
                        if jtype != "(none)" and jid_raw.isdigit():
                            jid = int(jid_raw)
                            Q.link_file_to_job(file_id, jtype, jid)
                            Q.log_event(
                                jtype, jid, "INGEST", "SUCCESS",
                                file_object_id=file_id,
                                agent="user",
                                detail=f"Ingested {Path(src).name} as {use_sel.value}",
                            )

                        fo = Q.get_file_object(file_id)
                        notify_success(f"Ingested: {Path(src).name}")

                        card = ui.card().classes(
                            "mt-3 p-4 rounded-xl border border-green-200 bg-green-50 w-full"
                        )
                        result_card.append(card)
                        with card:
                            ui.label("✅ File ingested successfully").classes(
                                "font-semibold text-green-700 mb-2"
                            )
                            if fo:
                                for label, val in [
                                    ("File ID", fo["id"]),
                                    ("UUID", fo["uuid"]),
                                    ("Size", f"{fo.get('size_bytes', 0):,} bytes"),
                                    ("SHA-256", fo.get("checksum_sha256", "—")),
                                    ("MIME", fo.get("mime_type", "—")),
                                    ("Use", fo.get("file_use", "—")),
                                    ("Stored at", fo.get("stored_path", "—")),
                                ]:
                                    with ui.row().classes("gap-2"):
                                        ui.label(f"{label}:").classes(
                                            "text-xs text-slate-500 w-16 shrink-0"
                                        )
                                        ui.label(str(val)).classes(
                                            "text-xs font-mono text-slate-700 break-all"
                                        )

                    except Exception as exc:
                        notify_error(f"Ingest error: {exc}")

                ui.button("Ingest File", on_click=_ingest).classes(
                    "bg-indigo-600 text-white rounded-lg px-4 py-2 mt-3 w-full"
                )

            # ── Upload panel ──────────────────────────────────────────────────
            with ui.card().classes("flex-1 min-w-80 p-5 rounded-xl border border-slate-200"):
                ui.label("Upload File").classes("font-semibold text-slate-700 mb-1")
                ui.label(
                    "Upload a file from your browser. It will be staged temporarily then "
                    "moved into artifacts/<Project Title>/ with the filename convention below."
                ).classes("text-sm text-slate-400 mb-3")

                upload_fmt = ui.select(
                    SUPPORTED_TYPES, label="Format", value="PDF"
                ).classes("w-full")
                upload_use = ui.select(
                    FILE_USES, label="File Use", value="ORIGINAL"
                ).classes("w-full")

                # ── Artifact metadata ────────────────────────────────────────
                ui.separator().classes("my-3")
                ui.label("Artifact Location (required)").classes(
                    "text-xs font-semibold text-slate-500 uppercase tracking-wider"
                )
                ui.label(
                    "File will be saved as: StudentInitials_SchoolName_GradeN_Subject.ext"
                ).classes("text-xs text-slate-400 mb-1")
                up_project_inp = ui.input(
                    "Project Title*", placeholder="e.g. Spring 2026 Braille Production"
                ).classes("w-full")
                up_student_inp = ui.input(
                    "Student Initials", placeholder="e.g. FrAn"
                ).classes("w-full")
                up_school_inp = ui.input(
                    "School Name", placeholder="e.g. LegacyJr"
                ).classes("w-full")
                up_grade_inp = ui.input(
                    "Grade Level", placeholder="e.g. 7"
                ).classes("w-full")
                up_subject_inp = ui.input(
                    "Subject", placeholder="e.g. Science"
                ).classes("w-full")

                ui.separator().classes("my-3")
                ui.label("Tools & Processes").classes(
                    "text-xs font-semibold text-slate-500 uppercase tracking-wider"
                )
                ui.label(
                    'Record the tool(s) and process(es) applied at this step — '
                    'e.g. tool: "brailleblaster", process: "OCR".  '
                    'Multiple tools and processes are allowed.'
                ).classes("text-xs text-slate-400 mb-1")

                up_tool_rows: list[dict] = []
                up_proc_rows: list[dict] = []

                ui.label("Tools").classes("text-xs font-medium text-slate-500 mt-1")
                up_tool_col = ui.column().classes("w-full gap-1")

                def _up_add_tool() -> None:
                    """Add a new tool input row to the upload panel's tool list."""
                    with up_tool_col:
                        with ui.row().classes("w-full gap-1 items-center") as row:
                            inp = ui.input(placeholder='e.g. brailleblaster').classes(
                                "flex-1 text-sm"
                            )
                            ref: dict = {"inp": inp, "row": row}
                            up_tool_rows.append(ref)
                            def _rm(r=ref) -> None:
                                """Remove a tool input row from the upload panel's tool list."""
                                r["row"].delete()
                                if r in up_tool_rows:
                                    up_tool_rows.remove(r)
                            ui.button(icon="close", on_click=_rm).props(
                                "flat dense round size=xs"
                            ).classes("text-slate-400")

                ui.button("+ Add Tool", on_click=_up_add_tool).props("flat dense").classes(
                    "text-xs text-indigo-600 self-start mt-1"
                )
                _up_add_tool()

                ui.label("Processes").classes("text-xs font-medium text-slate-500 mt-2")
                up_proc_col = ui.column().classes("w-full gap-1")

                def _up_add_proc() -> None:
                    """Add a new process input row to the upload panel's process list."""
                    with up_proc_col:
                        with ui.row().classes("w-full gap-1 items-center") as row:
                            inp = ui.input(
                                placeholder='e.g. OCR  (one process per row)'
                            ).classes("flex-1 text-sm")
                            ref = {"inp": inp, "row": row}
                            up_proc_rows.append(ref)
                            def _rm(r=ref) -> None:
                                """Remove a process input row from the upload panel's process list."""
                                r["row"].delete()
                                if r in up_proc_rows:
                                    up_proc_rows.remove(r)
                            ui.button(icon="close", on_click=_rm).props(
                                "flat dense round size=xs"
                            ).classes("text-slate-400")

                ui.button("+ Add Process", on_click=_up_add_proc).props("flat dense").classes(
                    "text-xs text-indigo-600 self-start mt-1"
                )
                _up_add_proc()
                ui.separator().classes("my-3")

                uploads_dir = Q.FILES_DIR

                def _handle_upload(event: events.UploadEventArguments) -> None:
                    """Handle a file upload event.

                    SEC-005: ``event.name`` comes directly from the browser and
                    may contain path separators or traversal sequences such as
                    ``../../etc/passwd``.  We sanitise it to a plain basename
                    before constructing any filesystem path.
                    """
                    if not up_project_inp.value.strip():
                        notify_error("Project Title is required for artifact storage")
                        return

                    # SEC-005: strip all directory components and reject suspicious names
                    raw_name   = Path(event.name).name          # basename only
                    safe_name  = "".join(
                        c for c in raw_name if c.isalnum() or c in "._- "
                    ).strip()
                    if not safe_name:
                        notify_error(
                            f"Upload rejected: filename '{event.name}' is not safe."
                        )
                        return

                    # Stage the upload temporarily in job_files/, then ingest to artifacts/
                    stage = uploads_dir / safe_name
                    stage.write_bytes(event.content.read())
                    try:
                        _up_tools = [r["inp"].value.strip() for r in up_tool_rows if r["inp"].value.strip()]
                        _up_procs = [r["inp"].value.strip() for r in up_proc_rows if r["inp"].value.strip()]
                        _up_extra: dict = {}
                        if _up_tools:
                            _up_extra["tools"] = _up_tools
                        if _up_procs:
                            _up_extra["processes"] = _up_procs

                        file_id = Q.ingest_file(
                            source_path=str(stage),
                            file_use=upload_use.value,
                            format_name=upload_fmt.value,
                            project_title=up_project_inp.value.strip(),
                            student_initials=up_student_inp.value.strip(),
                            school_name=up_school_inp.value.strip(),
                            grade_level=up_grade_inp.value.strip(),
                            subject=up_subject_inp.value.strip(),
                            extra_metadata=_up_extra or None,
                        )
                        # Remove the temporary staged copy
                        stage.unlink(missing_ok=True)
                        fo = Q.get_file_object(file_id)
                        notify_success(f"Uploaded and ingested: {event.name}")
                        if fo:
                            notify_success(f"Saved to: {fo.get('stored_path', '—')}")
                    except Exception as exc:
                        notify_error(f"Error: {exc}")

                ui.upload(on_upload=_handle_upload).classes("w-full").props(
                    "accept=.pdf,.docx,.epub,.brf,.pef,.txt,.html,.stl,.3mf,.gcode,.png,.jpg"
                )

        # ── Recent ingestions ──────────────────────────────────────────────────
        ui.label("Recent Ingestions").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-8 mb-2"
        )
        recent = Q.list_file_objects()[:20]
        if not recent:
            ui.label("No files ingested yet.").classes("text-slate-400 text-sm")
        else:
            with ui.card().classes("w-full rounded-xl border border-slate-200 overflow-hidden"):
                with ui.row().classes(
                    "px-4 py-2 bg-slate-50 text-xs font-semibold text-slate-500 "
                    "uppercase tracking-wider border-b"
                ):
                    ui.label("File Name").classes("flex-1")
                    ui.label("Use").classes("w-28")
                    ui.label("Format").classes("w-20")
                    ui.label("Size").classes("w-20 text-right")
                    ui.label("Ingested").classes("w-32")

                for f in recent:
                    sz = f.get("size_bytes") or 0
                    sz_str = (
                        f"{sz // 1_048_576} MB"
                        if sz >= 1_048_576
                        else f"{sz // 1024} KB"
                        if sz >= 1024
                        else f"{sz} B"
                    )
                    with ui.row().classes(
                        "items-center px-4 py-2 border-b border-slate-50 last:border-0 gap-2"
                    ):
                        ui.label(f["original_name"]).classes(
                            "flex-1 text-sm text-slate-700 truncate"
                        )
                        ui.label(f.get("file_use") or "—").classes(
                            "w-28 text-xs text-slate-500"
                        )
                        ui.label(f.get("format_name") or "—").classes(
                            "w-20 text-xs text-slate-500"
                        )
                        ui.label(sz_str).classes(
                            "w-20 text-right text-xs text-slate-500"
                        )
                        ui.label(str(f.get("created_at", ""))[:10]).classes(
                            "w-32 text-xs text-slate-400"
                        )

        # ── Structural map nodes (IMP-011) ───────────────────────────────────
        ui.label("Structural Map").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-8 mb-2"
        )
        with ui.card().classes("w-full p-5 rounded-xl border border-slate-200"):
            ui.label("Manage Structural Map Nodes").classes(
                "font-semibold text-slate-700 mb-2"
            )
            ui.label(
                "Capture section/chapter hierarchy linked to job files."
            ).classes("text-xs text-slate-400 mb-3")

            with ui.row().classes("gap-2 w-full items-end flex-wrap"):
                sm_job_type = ui.select(
                    ["braille", "lp_ebraille", "tactile", "print"],
                    value="braille",
                    label="Job Type",
                ).classes("w-44")
                sm_job_id = ui.input("Job ID", placeholder="numeric id").classes("w-32")

                nodes_box = ui.column().classes("w-full gap-1 mt-2")

                def _render_nodes() -> None:
                    nodes_box.clear()
                    raw = sm_job_id.value.strip()
                    if not raw.isdigit():
                        with nodes_box:
                            ui.label("Enter a numeric Job ID to view nodes.").classes(
                                "text-xs text-slate-400"
                            )
                        return

                    job_id = int(raw)
                    nodes = Q.list_struct_nodes(sm_job_type.value, job_id)
                    with nodes_box:
                        if not nodes:
                            ui.label("No structural nodes for this job yet.").classes(
                                "text-xs text-slate-400"
                            )
                            return

                        for node in nodes:
                            with ui.row().classes(
                                "items-center gap-2 border-b border-slate-100 py-2 last:border-0"
                            ):
                                ui.label(f"#{node['id']}").classes("text-xs text-slate-400 w-10")
                                ui.label(node.get("label") or "(no label)").classes(
                                    "text-sm text-slate-700 flex-1"
                                )
                                ui.badge(node.get("div_type") or "section").classes(
                                    "text-xs bg-slate-100 text-slate-700"
                                )
                                if node.get("file_name"):
                                    ui.label(node["file_name"]).classes(
                                        "text-xs text-indigo-600 max-w-52 truncate"
                                    )

                                def _del(node_id: int = node["id"]) -> None:
                                    Q.delete_struct_node(node_id)
                                    notify_success("Structural node deleted")
                                    _render_nodes()

                                ui.button("Delete", on_click=_del).props("flat dense").classes(
                                    "text-red-400 text-xs"
                                )

                def _open_add_node() -> None:
                    raw = sm_job_id.value.strip()
                    if not raw.isdigit():
                        notify_error("Enter a numeric Job ID first")
                        return

                    job_id = int(raw)
                    existing_nodes = Q.list_struct_nodes(sm_job_type.value, job_id)
                    parent_options = ["(root)"] + [
                        f"{n['id']}: {n['label']}" for n in existing_nodes
                    ]

                    with ui.dialog() as dlg, ui.card().classes("p-5 gap-3 w-[520px] max-w-full"):
                        ui.label("Add Structural Node").classes("text-lg font-semibold text-slate-800")
                        node_label = ui.input("Label*", placeholder="e.g. Chapter 1").classes("w-full")
                        div_type = ui.input("Division Type", value="section").classes("w-full")
                        order_num = ui.number("Order", value=len(existing_nodes), min=0).classes("w-full")
                        parent_sel = ui.select(parent_options, value="(root)", label="Parent").classes("w-full")
                        file_id = ui.input("File Object ID (optional)", placeholder="numeric id").classes("w-full")

                        with ui.row().classes("justify-end gap-2 mt-2"):
                            ui.button("Cancel", on_click=dlg.close).props("flat")

                            def _save_node() -> None:
                                label = node_label.value.strip()
                                if not label:
                                    notify_error("Label is required")
                                    return

                                parent_id = None
                                if parent_sel.value and parent_sel.value != "(root)":
                                    parent_id = int(str(parent_sel.value).split(":", 1)[0])

                                file_obj_id = None
                                file_raw = file_id.value.strip()
                                if file_raw:
                                    if not file_raw.isdigit():
                                        notify_error("File Object ID must be numeric")
                                        return
                                    file_obj_id = int(file_raw)

                                Q.add_struct_node(
                                    job_type=sm_job_type.value,
                                    job_id=job_id,
                                    label=label,
                                    parent_id=parent_id,
                                    div_type=div_type.value.strip() or "section",
                                    order_num=int(order_num.value or 0),
                                    file_object_id=file_obj_id,
                                )
                                notify_success("Structural node added")
                                dlg.close()
                                _render_nodes()

                            ui.button("Save", on_click=_save_node).classes("bg-indigo-600 text-white")

                    dlg.open()

                ui.button("Load Nodes", on_click=_render_nodes).props("flat dense").classes(
                    "text-slate-600"
                )
                ui.button("+ Add Node", on_click=_open_add_node).classes(
                    "bg-indigo-600 text-white"
                )

            _render_nodes()

Inventory panels

Purpose: inventory cards/forms for filament, paper, and electronics.

Inventory panels — Filament, Braille Paper, Electronics.

These replace the old models/inventory.py (which was mistakenly used as a UI file) and the partial ui/inventory.py stub.

electronics_page(content_area)

Render grouped electronics inventory with CRUD actions.

Source code in accessibility_mgr/ui/inventory_panels.py
def electronics_page(content_area: ui.element) -> None:
    """Render grouped electronics inventory with CRUD actions."""
    content_area.clear()
    with content_area:
        with ui.row().classes("items-center mb-4"):
            section_header(
                "Electronics Inventory", "Components, boards, wire, and hardware"
            )
            ui.element("div").classes("flex-1")

            def _new() -> None:
                """Open the add-component dialog."""
                def _do(data: dict) -> None:
                    """Add a new electronics component and refresh the page."""
                    Q.add_electronic(**data)
                    notify_success("Component added")
                    electronics_page(content_area)

                _elec_dialog(_do)

            ui.button("+ Add Component", on_click=_new).classes(
                "bg-blue-600 text-white rounded-lg px-4 py-2"
            )

        items = Q.list_electronics()
        cat_rows = Q.list_material_categories("elec_cat", active_only=False)

        # Group inventory by category
        by_cat: dict[str, list] = {}
        for item in items:
            by_cat.setdefault(item["category"], []).append(item)

        cat_labels = {r["value"]: r["label"] for r in cat_rows}
        rendered: set[str] = set()

        for row in cat_rows:
            cat = row["value"]
            cat_items = by_cat.get(cat, [])
            rendered.add(cat)

            ui.label(row["label"]).classes(
                "text-xs font-semibold text-slate-500 uppercase tracking-wider mt-4 mb-2"
            )

            if not cat_items:
                ui.label("No components in this category.").classes(
                    "text-sm text-slate-400 mb-2"
                )
                continue

            with ui.element("div").classes("grid gap-2 w-full"):
                for item in cat_items:
                    with ui.card().classes("p-3 rounded-lg border border-slate-200"):
                        with ui.row().classes("items-center gap-3"):
                            with ui.column().classes("flex-1 gap-0 min-w-0"):
                                ui.label(item["name"]).classes(
                                    "font-medium text-slate-800"
                                )
                                parts = []
                                if item.get("brand"):
                                    parts.append(item["brand"])
                                if item.get("spec"):
                                    parts.append(item["spec"])
                                if item.get("supplier"):
                                    parts.append(f"from {item['supplier']}")
                                if parts:
                                    ui.label(" · ".join(parts)).classes(
                                        "text-xs text-slate-400"
                                    )
                            with ui.column().classes("items-end gap-0 shrink-0"):
                                ui.label(
                                    f"{item.get('quantity', 0)} {item.get('unit', 'pcs')}"
                                ).classes("font-semibold text-slate-700")
                                if item.get("cost_each"):
                                    ui.label(f"${item['cost_each']:.2f} ea").classes(
                                        "text-xs text-slate-400"
                                    )
                            with ui.row().classes("gap-1 shrink-0"):
                                def _e(it: dict = item) -> None:
                                    """Open the edit dialog for an electronics component."""
                                    def _do(data: dict) -> None:
                                        """Update an existing electronics component and refresh the page."""
                                        Q.update_electronic(it["id"], **data)
                                        notify_success("Updated")
                                        electronics_page(content_area)

                                    _elec_dialog(_do, existing=it)

                                ui.button("Edit", on_click=_e).props(
                                    "flat dense"
                                ).classes("text-blue-600 text-xs")

                                def _d(it: dict = item) -> None:
                                    """Initiate deletion of an electronics component."""
                                    def _do() -> None:
                                        """Execute electronics component deletion."""
                                        Q.delete_electronic(it["id"])
                                        notify_success("Deleted")
                                        electronics_page(content_area)

                                    confirm_dialog(f"Delete '{it['name']}'?", _do)

                                ui.button("Del", on_click=_d).props(
                                    "flat dense"
                                ).classes("text-red-400 text-xs")

        # Show any legacy categories already in inventory but missing from lookup rows.
        for cat, cat_items in by_cat.items():
            if cat in rendered:
                continue

            ui.label(cat_labels.get(cat, cat.replace("_", " ").title())).classes(
                "text-xs font-semibold text-slate-500 uppercase tracking-wider mt-4 mb-2"
            )
            with ui.element("div").classes("grid gap-2 w-full"):
                for item in cat_items:
                    with ui.card().classes("p-3 rounded-lg border border-slate-200"):
                        with ui.row().classes("items-center gap-3"):
                            with ui.column().classes("flex-1 gap-0 min-w-0"):
                                ui.label(item["name"]).classes(
                                    "font-medium text-slate-800"
                                )
                                parts = []
                                if item.get("brand"):
                                    parts.append(item["brand"])
                                if item.get("spec"):
                                    parts.append(item["spec"])
                                if item.get("supplier"):
                                    parts.append(f"from {item['supplier']}")
                                if parts:
                                    ui.label(" · ".join(parts)).classes(
                                        "text-xs text-slate-400"
                                    )
                            with ui.column().classes("items-end gap-0 shrink-0"):
                                ui.label(
                                    f"{item.get('quantity', 0)} {item.get('unit', 'pcs')}"
                                ).classes("font-semibold text-slate-700")
                                if item.get("cost_each"):
                                    ui.label(f"${item['cost_each']:.2f} ea").classes(
                                        "text-xs text-slate-400"
                                    )
                            with ui.row().classes("gap-1 shrink-0"):
                                def _e(it: dict = item) -> None:
                                    """Open the edit dialog for a legacy electronics component."""
                                    def _do(data: dict) -> None:
                                        """Update a legacy electronics component and refresh the page."""
                                        Q.update_electronic(it["id"], **data)
                                        notify_success("Updated")
                                        electronics_page(content_area)

                                    _elec_dialog(_do, existing=it)

                                ui.button("Edit", on_click=_e).props(
                                    "flat dense"
                                ).classes("text-blue-600 text-xs")

                                def _d(it: dict = item) -> None:
                                    """Initiate deletion of a legacy electronics component."""
                                    def _do() -> None:
                                        """Execute legacy electronics component deletion."""
                                        Q.delete_electronic(it["id"])
                                        notify_success("Deleted")
                                        electronics_page(content_area)

                                    confirm_dialog(f"Delete '{it['name']}'?", _do)

                                ui.button("Del", on_click=_d).props(
                                    "flat dense"
                                ).classes("text-red-400 text-xs")

filament_page(content_area)

Render the filament inventory list and edit controls.

Source code in accessibility_mgr/ui/inventory_panels.py
def filament_page(content_area: ui.element) -> None:
    """Render the filament inventory list and edit controls."""
    content_area.clear()
    with content_area:
        with ui.row().classes("items-center mb-4"):
            section_header("Filament Inventory", "Track 3-D printer filament stock")
            ui.element("div").classes("flex-1")

            def _new() -> None:
                """Open the add-filament dialog."""
                def _do(data: dict) -> None:
                    """Add a new filament record and refresh the page."""
                    Q.add_filament(**data)
                    notify_success("Filament added")
                    filament_page(content_area)

                _filament_dialog(_do)

            ui.button("+ Add Filament", on_click=_new).classes(
                "bg-blue-600 text-white rounded-lg px-4 py-2"
            )

        filaments = Q.list_filaments()
        if not filaments:
            ui.label("No filament in inventory.").classes(
                "text-slate-400 text-lg text-center py-10"
            )
            return

        with ui.element("div").classes("grid gap-2 w-full"):
            for f in filaments:
                low = f.get("quantity_g", 0) < 100
                border = "border-amber-300 bg-amber-50" if low else "border-slate-200"
                with ui.card().classes(f"p-3 rounded-lg border {border}"):
                    with ui.row().classes("items-start gap-3"):
                        with ui.column().classes("flex-1 gap-0 min-w-0"):
                            ui.label(
                                f"{f['brand']}{f['color']} {f['filament_type']}"
                            ).classes("font-medium text-slate-800")
                            with ui.row().classes("gap-3 text-xs text-slate-400 flex-wrap"):
                                ui.label(f"{f.get('diameter_mm', 1.75)} mm")
                                if f.get("supplier"):
                                    ui.label(f"from {f['supplier']}")
                                if f.get("cost_per_kg"):
                                    ui.label(f"${f['cost_per_kg']:.2f}/kg")
                            if f.get("notes"):
                                ui.label(f["notes"]).classes(
                                    "text-xs text-slate-400 italic"
                                )

                        with ui.column().classes("items-end gap-0 shrink-0"):
                            qty_color = (
                                "text-amber-600 font-bold"
                                if low
                                else "text-slate-700 font-semibold"
                            )
                            ui.label(f"{f.get('quantity_g', 0):,.0f} g").classes(
                                qty_color
                            )
                            if low:
                                ui.badge("⚠ Low Stock").classes(
                                    "bg-amber-100 text-amber-700 text-xs rounded px-2"
                                )
                            with ui.row().classes("gap-1"):
                                def _edit(fil: dict = f) -> None:
                                    """Open the edit dialog for an existing filament."""
                                    def _do(data: dict) -> None:
                                        """Update an existing filament record and refresh the page."""
                                        Q.update_filament(fil["id"], **data)
                                        notify_success("Updated")
                                        filament_page(content_area)

                                    _filament_dialog(_do, existing=fil)

                                ui.button("Edit", on_click=_edit).props(
                                    "flat dense"
                                ).classes("text-blue-600 text-xs")

                                def _del(fil: dict = f) -> None:
                                    """Initiate deletion of a filament with confirmation."""
                                    import sqlite3

                                    def _do() -> None:
                                        """Execute filament deletion."""
                                        try:
                                            Q.delete_filament(fil["id"])
                                            notify_success("Deleted")
                                            filament_page(content_area)
                                        except sqlite3.IntegrityError:
                                            notify_error(
                                                "Cannot delete: referenced by print jobs. "
                                                "Delete those jobs first."
                                            )

                                    confirm_dialog(
                                        f"Delete {fil['brand']} {fil['color']}?", _do
                                    )

                                ui.button("Del", on_click=_del).props(
                                    "flat dense"
                                ).classes("text-red-400 text-xs")

paper_page(content_area)

Render the braille paper inventory list and edit controls.

Source code in accessibility_mgr/ui/inventory_panels.py
def paper_page(content_area: ui.element) -> None:
    """Render the braille paper inventory list and edit controls."""
    content_area.clear()
    with content_area:
        with ui.row().classes("items-center mb-4"):
            section_header("Braille Paper", "Track paper and label supplies")
            ui.element("div").classes("flex-1")

            def _new() -> None:
                """Open the add-paper dialog."""
                def _do(data: dict) -> None:
                    """Add a new paper record and refresh the page."""
                    Q.add_paper(**data)
                    notify_success("Paper added")
                    paper_page(content_area)

                _paper_dialog(_do)

            ui.button("+ Add Paper", on_click=_new).classes(
                "bg-blue-600 text-white rounded-lg px-4 py-2"
            )

        papers = Q.list_paper()
        if not papers:
            ui.label("No paper in inventory.").classes(
                "text-slate-400 text-lg text-center py-10"
            )
            return

        with ui.element("div").classes("grid gap-2 w-full"):
            for p in papers:
                low = p.get("quantity", 0) < 50
                border = "border-amber-300 bg-amber-50" if low else "border-slate-200"
                with ui.card().classes(f"p-3 rounded-lg border {border}"):
                    with ui.row().classes("items-center gap-3"):
                        with ui.column().classes("flex-1 gap-0 min-w-0"):
                            ui.label(
                                p["paper_type"].replace("_", " ").title()
                            ).classes("font-medium text-slate-800")
                            with ui.row().classes("gap-3 text-xs text-slate-400 flex-wrap"):
                                if p.get("size"):
                                    ui.label(f"Size: {p['size']}")
                                if p.get("label_type"):
                                    ui.label(f"Type: {p['label_type']}")
                                if p.get("supplier"):
                                    ui.label(f"from {p['supplier']}")
                        with ui.column().classes("items-end gap-0 shrink-0"):
                            qty_color = (
                                "text-amber-600 font-bold"
                                if low
                                else "text-slate-700 font-semibold"
                            )
                            ui.label(f"{p.get('quantity', 0):,} sheets").classes(
                                qty_color
                            )
                            if low:
                                ui.badge("⚠ Low").classes(
                                    "bg-amber-100 text-amber-700 text-xs rounded px-2"
                                )
                            with ui.row().classes("gap-1"):
                                def _e(pp: dict = p) -> None:
                                    """Open the edit dialog for an existing paper item."""
                                    def _do(data: dict) -> None:
                                        """Update an existing paper record and refresh the page."""
                                        Q.update_paper(pp["id"], **data)
                                        notify_success("Updated")
                                        paper_page(content_area)

                                    _paper_dialog(_do, existing=pp)

                                ui.button("Edit", on_click=_e).props(
                                    "flat dense"
                                ).classes("text-blue-600 text-xs")

                                def _d(pp: dict = p) -> None:
                                    """Initiate deletion of a paper item with confirmation."""
                                    def _do() -> None:
                                        """Execute paper deletion."""
                                        Q.delete_paper(pp["id"])
                                        notify_success("Deleted")
                                        paper_page(content_area)

                                    confirm_dialog(f"Delete {pp['paper_type']}?", _do)

                                ui.button("Del", on_click=_d).props(
                                    "flat dense"
                                ).classes("text-red-400 text-xs")

Job components

Purpose: shared job table/form/status UI blocks.

Shared job detail components for metadata and event-log rendering.

export_job_summary(*, job_type, job, step_order, step_labels)

Generate and download a print-ready HTML summary for a job.

Source code in accessibility_mgr/ui/job_components.py
def export_job_summary(
        *,
        job_type: str,
        job: dict,
        step_order: list[str],
        step_labels: dict[str, str],
) -> None:
        """Generate and download a print-ready HTML summary for a job."""
        job_id = int(job["id"])
        metadata = Q.list_job_metadata(job_type, job_id)
        events = Q.list_events_for_job(job_type, job_id)
        files = Q.list_files_for_job(job_type, job_id)

        step_rows: list[str] = []
        for step in step_order:
                completed = bool(job.get(step, 0))
                completed_at = ""
                for event in events:
                        if event.get("event_type") == "STEP_COMPLETE" and event.get("step_key") == step:
                                completed_at = str(event.get("event_datetime") or "")[:19]
                                break
                step_rows.append(
                        "<tr>"
                        f"<td>{escape(step_labels.get(step, step))}</td>"
                        f"<td>{'Yes' if completed else 'No'}</td>"
                        f"<td>{escape(completed_at or '-')}</td>"
                        "</tr>"
                )

        metadata_rows = "".join(
                f"<tr><td>{escape(str(k))}</td><td>{escape(str(v))}</td></tr>"
                for k, v in sorted(metadata.items())
        ) or "<tr><td colspan='2'>No metadata</td></tr>"

        files_rows = "".join(
                "<tr>"
                f"<td>{escape(str(f.get('original_name') or '-'))}</td>"
                f"<td>{escape(str(f.get('file_use') or '-'))}</td>"
                f"<td>{escape(str(f.get('checksum_sha256') or '-'))}</td>"
                "</tr>"
                for f in files
        ) or "<tr><td colspan='3'>No linked files</td></tr>"

        event_rows = "".join(
                "<tr>"
                f"<td>{escape(str(e.get('event_datetime') or '')[:19])}</td>"
                f"<td>{escape(str(e.get('event_type') or '-'))}</td>"
                f"<td>{escape(str(e.get('detail') or '-'))}</td>"
                "</tr>"
                for e in events[:200]
        ) or "<tr><td colspan='3'>No events</td></tr>"

        core_rows = []
        for key in [
                "title",
                "object_name",
                "requester",
                "request_date",
                "due_date",
                "priority",
                "delivery_date",
                "delivery_method",
                "delivery_recipient",
                "delivery_notes",
                "created_at",
                "printed_at",
        ]:
                if key in job and job.get(key) not in (None, ""):
                        core_rows.append(
                                f"<tr><td>{escape(key)}</td><td>{escape(str(job.get(key)))}</td></tr>"
                        )
        core_table = "".join(core_rows) or "<tr><td colspan='2'>No core fields</td></tr>"

        html = f"""
<!doctype html>
<html lang='en'>
<head>
    <meta charset='utf-8'>
    <title>Job Summary {job_type} #{job_id}</title>
    <style>
        body {{ font-family: Arial, sans-serif; margin: 24px; color: #1f2937; }}
        h1, h2 {{ margin: 0 0 12px 0; }}
        h2 {{ margin-top: 20px; font-size: 18px; }}
        table {{ width: 100%; border-collapse: collapse; margin-top: 8px; }}
        th, td {{ border: 1px solid #d1d5db; padding: 6px 8px; text-align: left; font-size: 12px; }}
        th {{ background: #f8fafc; }}
        .meta {{ color: #6b7280; font-size: 12px; }}
    </style>
</head>
<body>
    <h1>Job Summary</h1>
    <div class='meta'>Type: {escape(job_type)} | ID: {job_id}</div>

    <h2>Core Metadata</h2>
    <table>
        <tr><th>Field</th><th>Value</th></tr>
        {core_table}
    </table>

    <h2>Dublin Core and Additional Metadata</h2>
    <table>
        <tr><th>Key</th><th>Value</th></tr>
        {metadata_rows}
    </table>

    <h2>Workflow Steps</h2>
    <table>
        <tr><th>Step</th><th>Completed</th><th>Completion Timestamp</th></tr>
        {''.join(step_rows)}
    </table>

    <h2>Attached Files and Checksums</h2>
    <table>
        <tr><th>File</th><th>Use</th><th>SHA-256</th></tr>
        {files_rows}
    </table>

    <h2>Event Log</h2>
    <table>
        <tr><th>Timestamp</th><th>Event</th><th>Detail</th></tr>
        {event_rows}
    </table>
</body>
</html>
"""

        filename = f"{job_type}_job_{job_id}_summary.html"
        ui.download(html.encode("utf-8"), filename)

open_metadata_dialog(job_type, job_id, on_done)

Open the shared metadata editor dialog for a job.

Source code in accessibility_mgr/ui/job_components.py
def open_metadata_dialog(job_type: str, job_id: int, on_done: Callable[[], None]) -> None:
    """Open the shared metadata editor dialog for a job."""
    existing_meta = Q.list_job_metadata(job_type, job_id)
    option_groups = get_option_groups()
    dc_keys = get_dublin_core_keys()
    dc_examples = get_dublin_core_examples()
    non_dc_keys = get_non_dc_allowed_keys()

    with ui.dialog() as dlg, ui.card().classes(
        "p-6 gap-4 w-[600px] max-w-full max-h-[90vh] overflow-y-auto"
    ):
        ui.label("Descriptive Metadata").classes("text-xl font-bold text-slate-800")
        ui.label(
            "Dublin Core plus controlled eBraille and METS/PREMIS fields."
        ).classes("text-slate-500 text-sm")

        def _show_options() -> None:
            with ui.dialog() as od, ui.card().classes(
                "p-5 gap-3 w-[720px] max-w-full max-h-[85vh] overflow-y-auto"
            ):
                ui.label("Potential Metadata Options").classes(
                    "text-lg font-bold text-slate-800"
                )
                ui.label(
                    "Use Admin Settings -> Metadata Options to add or remove allowed keys."
                ).classes("text-xs text-slate-500")
                for group, keys in option_groups.items():
                    ui.separator()
                    ui.label(group).classes(
                        "text-sm font-semibold text-slate-600 uppercase tracking-wider"
                    )
                    with ui.row().classes("gap-2 flex-wrap"):
                        for key in keys:
                            ui.badge(key).classes(
                                "bg-slate-100 text-slate-700 text-xs rounded px-2 py-1"
                            )
                with ui.row().classes("justify-end mt-2"):
                    ui.button("Close", on_click=od.close).classes("bg-slate-700 text-white")
            od.open()

        ui.button("Potential Options", on_click=_show_options).props("flat dense").classes(
            "text-indigo-600 text-sm self-start"
        )

        meta_rows: dict[str, ui.input] = {}
        with ui.grid(columns=2).classes("gap-2 w-full"):
            for key in dc_keys:
                with ui.column().classes("gap-0"):
                    inp = ui.input(key, value=existing_meta.get(key, "")).classes(
                        "w-full font-mono text-sm"
                    )
                    ui.label(dc_examples.get(key, "")).classes("text-[11px] text-slate-400")
                    meta_rows[key] = inp

        ui.separator()
        ui.label("Additional Allowed Fields").classes("text-sm font-medium text-slate-600")
        ui.label("Choose keys from the approved eBraille and METS/PREMIS list.").classes(
            "text-xs text-slate-400"
        )

        extra_rows: list[dict[str, ui.element]] = []
        extra_box = ui.column().classes("w-full gap-2")

        def _add_extra_row(initial_key: str = "", initial_val: str = "") -> None:
            with extra_box:
                with ui.row().classes("gap-2 w-full items-center") as row:
                    key_sel = ui.select(
                        non_dc_keys,
                        label="Key",
                        value=initial_key if initial_key in non_dc_keys else None,
                    ).classes("w-64")
                    val_inp = ui.input("Value", value=initial_val).classes("flex-1")
                    ref = {"row": row, "key": key_sel, "value": val_inp}
                    extra_rows.append(ref)

                    def _remove(r: dict[str, ui.element] = ref) -> None:
                        r["row"].delete()
                        if r in extra_rows:
                            extra_rows.remove(r)

                    ui.button("x", on_click=_remove).props("flat dense").classes("text-red-400")

        ui.button("+ Add Field", on_click=lambda: _add_extra_row()).props("flat dense").classes(
            "text-indigo-600 text-sm self-start"
        )

        for key, value in existing_meta.items():
            if key not in dc_keys and key in non_dc_keys:
                _add_extra_row(initial_key=key, initial_val=value)

        with ui.row().classes("justify-end gap-3 mt-4"):
            ui.button("Close", on_click=dlg.close).props("flat").classes("text-slate-500")

            def _save_all() -> None:
                saved_keys: list[str] = []
                for key, inp in meta_rows.items():
                    value = inp.value.strip()
                    if value:
                        Q.set_job_metadata(job_type, job_id, key, value)
                        saved_keys.append(key)
                    else:
                        Q.delete_job_metadata(job_type, job_id, key)

                for key in non_dc_keys:
                    Q.delete_job_metadata(job_type, job_id, key)
                for row in extra_rows:
                    key = (row["key"].value or "").strip()
                    value = (row["value"].value or "").strip()
                    if key and value and key in non_dc_keys:
                        Q.set_job_metadata(job_type, job_id, key, value)
                        saved_keys.append(key)

                Q.log_event(
                    job_type,
                    job_id,
                    "METADATA_UPDATE",
                    "SUCCESS",
                    agent="user",
                    detail=f"Metadata updated: {len(saved_keys)} field(s)",
                    extra_metadata={"updated_keys": saved_keys},
                )
                notify_success("Metadata saved")
                dlg.close()
                on_done()

            ui.button("Save All", on_click=_save_all).classes("bg-blue-600 text-white")

    dlg.open()

render_event_log(*, job_type, job_id, step_labels, on_done, title='Provenance / Event Log', subtitle='', step_badge_classes='text-xs bg-blue-50 text-blue-700 rounded px-1')

Render the shared event log card body with add-note dialog.

Source code in accessibility_mgr/ui/job_components.py
def render_event_log(
    *,
    job_type: str,
    job_id: int,
    step_labels: dict[str, str],
    on_done: Callable[[], None],
    title: str = "Provenance / Event Log",
    subtitle: str = "",
    step_badge_classes: str = "text-xs bg-blue-50 text-blue-700 rounded px-1",
) -> None:
    """Render the shared event log card body with add-note dialog."""
    with ui.row().classes("items-center mb-3"):
        ui.label(title).classes("font-semibold text-slate-700 flex-1")
        if subtitle:
            ui.label(subtitle).classes("text-xs text-slate-400")

        def _add_note() -> None:
            with ui.dialog() as nd, ui.card().classes("p-5 gap-3 w-96"):
                ui.label("Add Note Event").classes("font-semibold text-slate-800")
                note_txt = ui.textarea("Note").classes("w-full").props("rows=3")
                agent_txt = ui.input("Agent/Author", value="user").classes("w-full")
                with ui.row().classes("justify-end gap-2"):
                    ui.button("Cancel", on_click=nd.close).props("flat")

                    def _save_note() -> None:
                        Q.log_event(
                            job_type,
                            job_id,
                            "NOTE",
                            "SUCCESS",
                            agent=agent_txt.value.strip() or "user",
                            detail=note_txt.value.strip(),
                        )
                        nd.close()
                        on_done()

                    ui.button("Save", on_click=_save_note).classes("bg-slate-700 text-white")
            nd.open()

        ui.button("+ Add Note", on_click=_add_note).props("flat dense").classes(
            "text-slate-600 text-sm"
        )

    events = Q.list_events_for_job(job_type, job_id)
    if not events:
        ui.label("No events recorded.").classes("text-slate-400 text-sm")
        return

    for ev in events:
        outcome = ev.get("event_outcome", "SUCCESS")
        text_class = OUTCOME_COLORS.get(outcome, "text-slate-700")
        with ui.row().classes(
            "items-start gap-3 py-2 border-b border-slate-50 last:border-0"
        ):
            with ui.column().classes("gap-0 w-36 shrink-0"):
                ui.label(str(ev.get("event_datetime", ""))[:19]).classes(
                    "text-xs text-slate-400 font-mono"
                )
                ui.label(ev.get("agent", "system")).classes(
                    "text-xs text-slate-400 italic"
                )
            with ui.column().classes("flex-1 gap-0"):
                with ui.row().classes("gap-2 items-center"):
                    ui.badge(ev["event_type"]).classes(
                        "text-xs bg-slate-100 text-slate-700 rounded px-1"
                    )
                    if ev.get("step_key"):
                        ui.badge(step_labels.get(ev["step_key"], ev["step_key"])).classes(
                            step_badge_classes
                        )
                    if ev.get("file_name"):
                        ui.badge(ev["file_name"]).classes(
                            "text-xs bg-indigo-50 text-indigo-700 rounded px-1"
                        )
                if ev.get("detail"):
                    ui.label(ev["detail"]).classes(f"text-sm {text_class}")

Lineage page

Purpose: lineage graph and file/job provenance views.

Lineage viewer — visualises derivative relationships and provenance chains using data from the file_object, job_file_link, and metadata_event tables.

lineage_page(content_area)

Render the Asset Lineage Viewer.

Source code in accessibility_mgr/ui/lineage.py
def lineage_page(content_area: ui.element) -> None:
    """Render the Asset Lineage Viewer."""
    _seed_dev_provenance()
    page_size = 50
    state = {"page": 1}

    content_area.clear()

    with content_area:
        section_header(
            "Lineage Viewer",
            "Derivative relationships and provenance chains across all jobs",
        )

        files = Q.list_file_objects(limit=300)
        _GRAPH_NODE_LIMIT = 50
        _JOB_LOAD_LIMIT   = 100

        if not files:
            with ui.card().classes(
                "p-10 text-center border border-slate-200 rounded-xl w-full"
            ):
                ui.label("No files ingested yet.").classes(
                    "text-slate-400 text-lg"
                )
                ui.label(
                    "Attach files to production jobs to see lineage here."
                ).classes("text-slate-400 text-sm mt-1")
            return

        ui.label(f"{len(files)} file(s) in registry").classes(
            "text-sm text-slate-400 mb-4"
        )

        braille_jobs = {
            job["id"]: job["title"] for job in Q.list_braille_jobs(limit=_JOB_LOAD_LIMIT)
        }

        lp_jobs = {
            job["id"]: job["title"] for job in Q.list_lp_jobs(limit=_JOB_LOAD_LIMIT)
        }

        tactile_jobs = {
            job["id"]: job["title"]
            for job in Q.list_tactile_jobs(limit=_JOB_LOAD_LIMIT)
        }

        print_jobs = {
            job["id"]: job.get("object_name", "3-D Print Job")
            for job in Q.list_print_jobs(limit=_JOB_LOAD_LIMIT)
        }

        mermaid_lines = ["graph TD"]
        seen_nodes: set[str] = set()
        seen_edges: set[str] = set()

        for file_obj in files:
            fnode = f"F{file_obj['id']}"
            label = file_obj["original_name"][:30].replace('"', "")
            use = file_obj.get("file_use", "ORIGINAL")
            shape = f'["{label}\\n{use}"]'
            mermaid_lines.append(f"  {fnode}{shape}")

        _append_job_edges(
            mermaid_lines,
            seen_nodes,
            seen_edges,
            files,
            braille_jobs,
            "BJ",
            "Braille",
            "braille",
        )

        _append_job_edges(
            mermaid_lines,
            seen_nodes,
            seen_edges,
            files,
            lp_jobs,
            "LP",
            "LP",
            "lp_ebraille",
        )

        _append_job_edges(
            mermaid_lines,
            seen_nodes,
            seen_edges,
            files,
            tactile_jobs,
            "TG",
            "Tactile",
            "tactile",
        )

        _append_job_edges(
            mermaid_lines,
            seen_nodes,
            seen_edges,
            files,
            print_jobs,
            "PJ",
            "3-D Print",
            "print",
        )

        if len(mermaid_lines) > 1:
            graph_truncated = (len(mermaid_lines) - 1) > _GRAPH_NODE_LIMIT
            display_lines = mermaid_lines[:_GRAPH_NODE_LIMIT + 1] if graph_truncated else mermaid_lines
            with ui.card().classes("p-4 rounded-xl border border-slate-200 w-full mb-6"):
                ui.label("Lineage Graph").classes("font-semibold text-slate-700 mb-3")
                if graph_truncated:
                    ui.label(
                        f"⚠ Graph truncated to {_GRAPH_NODE_LIMIT} nodes. "
                        "Use a more specific date or job filter to narrow the view."
                    ).classes("text-xs text-amber-600 mb-2")
                ui.mermaid("\n".join(display_lines))

            with ui.card().classes("p-4 rounded-xl border border-slate-200 w-full mb-6"):
                ui.label("File Registry").classes("font-semibold text-slate-700 mb-3")

                pager_row = ui.row().classes("items-center gap-2 mb-2")
                file_table = ui.column().classes("w-full")

                def _render_file_page() -> None:
                    rows = Q.list_file_objects(
                        limit=page_size + 1,
                        offset=(state["page"] - 1) * page_size,
                    )
                    has_next = len(rows) > page_size
                    page_rows = rows[:page_size]

                    pager_row.clear()
                    with pager_row:
                        ui.button("Prev", on_click=lambda: _set_page(state["page"] - 1)).props(
                            "flat dense"
                        ).classes("text-slate-600").props("disable" if state["page"] <= 1 else "")
                        ui.label(f"Page {state['page']}").classes("text-sm text-slate-500")
                        ui.button("Next", on_click=lambda: _set_page(state["page"] + 1)).props(
                            "flat dense"
                        ).classes("text-slate-600").props("disable" if not has_next else "")

                    file_table.clear()
                    with file_table:
                        with ui.card().classes(
                            "w-full rounded-xl border border-slate-200 overflow-hidden"
                        ):
                            with ui.row().classes(
                                "px-4 py-2 bg-slate-50 text-xs font-semibold text-slate-500 "
                                "uppercase tracking-wider border-b"
                            ):
                                ui.label("File Name").classes("flex-1")
                                ui.label("Use").classes("w-28")
                                ui.label("Format").classes("w-20")
                                ui.label("Size").classes("w-20 text-right")
                                ui.label("SHA-256").classes("w-40")
                                ui.label("Ingested").classes("w-32")

                            for file_obj in page_rows:
                                size = file_obj.get("size_bytes") or 0
                                size_str = (
                                    f"{size // 1_048_576} MB"
                                    if size >= 1_048_576
                                    else f"{size // 1024} KB"
                                    if size >= 1024
                                    else f"{size} B"
                                )

                                checksum = str(file_obj.get("checksum_sha256") or "—")
                                checksum_short = checksum[:12] + "…" if len(checksum) > 12 else checksum

                                with ui.row().classes(
                                    "items-center px-4 py-2 border-b border-slate-50 last:border-0 gap-2"
                                ):
                                    with ui.column().classes("flex-1 gap-0 min-w-0"):
                                        ui.label(file_obj["original_name"]).classes(
                                            "text-sm text-slate-700 truncate"
                                        )
                                        ui.label(file_obj.get("mime_type") or "").classes(
                                            "text-xs text-slate-400"
                                        )

                                    ui.label(file_obj.get("file_use") or "—").classes(
                                        "w-28 text-xs text-slate-500"
                                    )
                                    ui.label(file_obj.get("format_name") or "—").classes(
                                        "w-20 text-xs text-slate-500"
                                    )
                                    ui.label(size_str).classes("w-20 text-right text-xs text-slate-500")
                                    ui.label(checksum_short).classes(
                                        "w-40 text-xs text-slate-500 font-mono"
                                    )
                                    ui.label(str(file_obj.get("created_at") or "")[:10]).classes(
                                        "w-32 text-xs text-slate-500"
                                    )

                def _set_page(page: int) -> None:
                    state["page"] = max(1, page)
                    _render_file_page()

                _render_file_page()

LP/eBraille page

Purpose: large print/eBraille job handling and status progression.

LP / eBraille / EPUB3-DAISY jobs panel.

Changes applied (see fix_specs.json): FIX-003 _save_all in metadata dialog now calls Q.log_event (persisted to DB). FIX-008 _ingest_dialog pre-populates project context from job metadata so files land in artifacts// not job_files/.. FIX-016 Delivered step opens delivery confirmation dialog instead of direct toggle.

Metadata editor

Purpose: edit and inspect structured metadata fields.

Metadata editor — edit and inspect Dublin Core and custom metadata for any job.

Changes applied (see fix_specs.json): FIX-003 Metadata saves now call Q.log_event (persisted to DB) instead of the in-memory MetadataAuditService. MetadataAuditService import removed.

metadata_editor_page(content_area)

Render the metadata editor page.

Source code in accessibility_mgr/ui/metadata_editor.py
def metadata_editor_page(content_area: ui.element) -> None:
    """Render the metadata editor page."""
    content_area.clear()
    with content_area:
        section_header(
            "Metadata Editor",
            "Edit Dublin Core and custom metadata for any job",
        )

        braille_jobs = Q.list_braille_jobs()
        lp_jobs = Q.list_lp_jobs()
        tactile_jobs = Q.list_tactile_jobs()
        print_jobs = Q.list_print_jobs()

        opts: list[str] = []
        opt_map: dict[str, tuple[str, int]] = {}

        for job in braille_jobs:
            label = f"[Braille #{job['id']}] {job['title']}"
            opts.append(label)
            opt_map[label] = ("braille", job["id"])

        for job in lp_jobs:
            label = f"[LP #{job['id']}] {job['title']}"
            opts.append(label)
            opt_map[label] = ("lp_ebraille", job["id"])

        for job in tactile_jobs:
            label = f"[Tactile #{job['id']}] {job['title']}"
            opts.append(label)
            opt_map[label] = ("tactile", job["id"])

        for job in print_jobs:
            obj = job.get("object_name") or job.get("file_name") or "Untitled"
            label = f"[Print #{job['id']}] {obj}"
            opts.append(label)
            opt_map[label] = ("print", job["id"])

        if not opts:
            ui.label("No jobs found. Create a production job first.").classes(
                "text-slate-400 text-sm"
            )
            return

        job_sel = ui.select(opts, label="Select Job", value=opts[0]).classes(
            "w-full max-w-xl"
        )

        editor_container = ui.column().classes("w-full mt-4")
        jtype0, jid0 = opt_map[opts[0]]
        _render_editor(jtype0, jid0, editor_container)

        def _on_job_change(_: object) -> None:
            val = job_sel.value
            if val in opt_map:
                jt, ji = opt_map[val]
                _render_editor(jt, ji, editor_container)

        job_sel.on("update:model-value", _on_job_change)

Metadata options

Purpose: metadata option controls and configured value lists.

Metadata option catalog for UI forms.

Defaults live in this file and can be overridden/extended from Admin Settings, where metadata keys are persisted in material_category sections.

get_allowed_metadata_keys()

Return all allowed metadata keys across all option groups.

Source code in accessibility_mgr/ui/metadata_options.py
def get_allowed_metadata_keys() -> list[str]:
    """Return all allowed metadata keys across all option groups."""
    groups = get_option_groups()
    return list(groups.get("Dublin Core", [])) + get_non_dc_allowed_keys()

get_dublin_core_examples()

Return examples keyed by current DC keys (blank when no built-in example exists).

Source code in accessibility_mgr/ui/metadata_options.py
def get_dublin_core_examples() -> dict[str, str]:
    """Return examples keyed by current DC keys (blank when no built-in example exists)."""
    return {k: DUBLIN_CORE_EXAMPLES.get(k, "") for k in get_dublin_core_keys()}

get_dublin_core_keys()

Return the active Dublin Core metadata key list.

Source code in accessibility_mgr/ui/metadata_options.py
def get_dublin_core_keys() -> list[str]:
    """Return the active Dublin Core metadata key list."""
    return list(get_option_groups().get("Dublin Core", []))

get_non_dc_allowed_keys()

Return non-Dublin-Core keys (eBraille Profile + METS/PREMIS).

Source code in accessibility_mgr/ui/metadata_options.py
def get_non_dc_allowed_keys() -> list[str]:
    """Return non-Dublin-Core keys (eBraille Profile + METS/PREMIS)."""
    groups = get_option_groups()
    return list(groups.get("eBraille Profile", [])) + list(groups.get("METS / PREMIS", []))

get_option_groups()

Return metadata option groups loaded from DB or defaults.

Source code in accessibility_mgr/ui/metadata_options.py
def get_option_groups() -> dict[str, list[str]]:
    """Return metadata option groups loaded from DB or defaults."""
    return _load_runtime_option_groups()

Operations dashboard

Purpose: operations-oriented runtime metrics and controls.

Operations analytics dashboard.

operations_dashboard_page(content_area)

Render operational analytics dashboard.

Source code in accessibility_mgr/ui/operations_dashboard.py
def operations_dashboard_page(content_area: ui.element) -> None:
    """Render operational analytics dashboard."""
    content_area.clear()

    def _render() -> None:
        content_area.clear()

        summary = _analytics.summarize()
        sla_summary = _sla.health_summary()
        topo = _dag.topology()
        executable = _dag.executable_workflows()
        organizations = _tenants.list_organizations()
        memberships = _tenants.list_memberships()
        retention_records = _retention.evaluate_retention()
        audit_events = _audit.list_events()
        workers = _workers.list_nodes()
        stream_events = _events.list_events()
        subscriptions = _events.list_subscriptions()

        with content_area:
            section_header(
                "Operations Dashboard",
                "Operational KPI, workflow, and governance controls",
            )

            with ui.row().classes("w-full gap-3 flex-wrap mb-4"):
                ui.button("Run SLA Evaluation", on_click=lambda: (_sla.evaluate_slas(), _render())).classes(
                    "bg-blue-600 text-white"
                )

                def _complete_next() -> None:
                    if executable:
                        _dag.complete(executable[0]["workflow_name"])
                    _render()

                ui.button("Complete Next DAG Step", on_click=_complete_next).classes(
                    "bg-green-600 text-white"
                )

                def _publish_event_form() -> None:
                    with ui.dialog() as d, ui.card().classes("p-6 gap-3 w-[440px]"):
                        ui.label("Publish Platform Event").classes("text-lg font-bold text-slate-800")
                        etype_inp = ui.input("Event Type*", placeholder="e.g. job_completed, pipeline_failed").classes("w-full")
                        payload_inp = ui.textarea("Payload (JSON)", placeholder='{"job_id": 42, "status": "ok"}').classes("w-full")

                        def _do_publish() -> None:
                            import json as _json
                            etype = etype_inp.value.strip()
                            if not etype:
                                notify_error("Event type is required.")
                                return
                            raw = payload_inp.value.strip()
                            try:
                                payload = _json.loads(raw) if raw else {}
                            except ValueError:
                                notify_error("Payload must be valid JSON or empty.")
                                return
                            _events.publish(event_type=etype, payload=payload)
                            d.close()
                            notify_success(f"Event '{etype}' published.")
                            _render()

                        with ui.row().classes("justify-end gap-2 mt-2"):
                            ui.button("Cancel", on_click=d.close).props("flat").classes("text-slate-500")
                            ui.button("Publish", on_click=_do_publish).classes("bg-amber-600 text-white")
                    d.open()

                ui.button("+ Publish Event", on_click=_publish_event_form).classes("bg-amber-600 text-white")

                def _register_worker_form() -> None:
                    with ui.dialog() as d, ui.card().classes("p-6 gap-3 w-[440px]"):
                        ui.label("Register Worker Node").classes("text-lg font-bold text-slate-800")
                        nid_inp = ui.input("Node ID*", placeholder="e.g. worker-prod-01").classes("w-full")
                        host_inp = ui.input("Hostname*", placeholder="e.g. prod-server-01.internal").classes("w-full")

                        def _do_register() -> None:
                            nid = nid_inp.value.strip()
                            host = host_inp.value.strip()
                            if not nid or not host:
                                notify_error("Node ID and hostname are required.")
                                return
                            _workers.register_node(node_id=nid, hostname=host)
                            _audit.record_event(event_type="worker_registered", actor="operator",
                                                payload={"node_id": nid, "hostname": host})
                            d.close()
                            notify_success(f"Worker '{nid}' registered.")
                            _render()

                        with ui.row().classes("justify-end gap-2 mt-2"):
                            ui.button("Cancel", on_click=d.close).props("flat").classes("text-slate-500")
                            ui.button("Register", on_click=_do_register).classes("bg-slate-700 text-white")
                    d.open()

                ui.button("+ Register Worker", on_click=_register_worker_form).classes("bg-slate-700 text-white")

            with ui.grid(columns=4).classes("w-full gap-4 mb-6"):
                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("Tracked Metrics").classes("text-sm text-slate-500")
                    ui.label(str(summary["total_metrics"])).classes("text-3xl font-bold text-slate-700")

                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("SLA Breaches").classes("text-sm text-slate-500")
                    ui.label(str(sla_summary["sla_breaches"])).classes("text-3xl font-bold text-red-600")

                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("Workers Online").classes("text-sm text-slate-500")
                    online = len([n for n in workers if n.get("status") == "online"])
                    ui.label(str(online)).classes("text-3xl font-bold text-green-600")

                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("Stream Events").classes("text-sm text-slate-500")
                    ui.label(str(len(stream_events))).classes("text-3xl font-bold text-indigo-600")

            with ui.grid(columns=2).classes("w-full gap-4"):
                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("Workflow DAG").classes("text-base font-semibold text-slate-700 mb-2")
                    ui.label(f"Executable: {len(executable)}").classes("text-xs text-slate-500 mb-2")
                    for node in topo:
                        with ui.row().classes("items-center justify-between border-b border-slate-100 py-1"):
                            ui.label(node["workflow_name"]).classes("text-sm text-slate-700")
                            ui.badge(node["status"]).classes("text-xs bg-slate-100 text-slate-700")

                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("SLA Tracking").classes("text-base font-semibold text-slate-700 mb-2")
                    records = _sla.evaluate_slas()
                    if not records:
                        ui.label("No tracked workflows.").classes("text-sm text-slate-400")
                    for record in records:
                        with ui.row().classes("items-center justify-between border-b border-slate-100 py-1"):
                            ui.label(f"{record['workflow_name']} (asset {record['asset_id']})").classes(
                                "text-sm text-slate-700"
                            )
                            ui.badge("breached" if record["breached"] else "healthy").classes(
                                "text-xs "
                                + ("bg-red-100 text-red-700" if record["breached"] else "bg-green-100 text-green-700")
                            )

                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("Workers & Event Stream").classes("text-base font-semibold text-slate-700 mb-2")
                    ui.label(f"Subscriptions: {len(subscriptions)}").classes("text-xs text-slate-500")
                    ui.label(f"Events: {len(stream_events)}").classes("text-xs text-slate-500 mb-2")
                    for node in workers[-5:]:
                        with ui.row().classes("items-center justify-between border-b border-slate-100 py-1"):
                            ui.label(node["node_id"]).classes("text-sm text-slate-700")
                            ui.badge(node["status"]).classes("text-xs bg-slate-100 text-slate-700")

                with ui.card().classes("p-5 rounded-xl border border-slate-200"):
                    ui.label("Tenancy & Retention").classes("text-base font-semibold text-slate-700 mb-2")

                    def _add_org_form() -> None:
                        with ui.dialog() as d, ui.card().classes("p-6 gap-3 w-[420px]"):
                            ui.label("Add Organization").classes("text-lg font-bold text-slate-800")
                            org_name = ui.input("Organization Name*", placeholder="e.g. District 42 Special Ed").classes("w-full")
                            role_sel = ui.select(["operator", "admin", "viewer"], value="operator", label="Initial member role").classes("w-full")
                            username_inp = ui.input("Initial member username*", placeholder="e.g. jdoe").classes("w-full")

                            def _submit_org() -> None:
                                name = org_name.value.strip()
                                username = username_inp.value.strip()
                                if not name:
                                    notify_error("Organization name is required.")
                                    return
                                if not username:
                                    notify_error("Member username is required.")
                                    return
                                org = _tenants.create_organization(name)
                                _tenants.add_member(username=username, organization_id=org.organization_id, role=role_sel.value)
                                _audit.record_event(event_type="organization_created", actor=username, payload={"org": name, "role": role_sel.value})
                                d.close()
                                notify_success(f"Organization '{name}' created.")
                                _render()

                            with ui.row().classes("justify-end gap-2 mt-2"):
                                ui.button("Cancel", on_click=d.close).props("flat").classes("text-slate-500")
                                ui.button("Create", on_click=_submit_org).classes("bg-blue-600 text-white")
                        d.open()

                    ui.button("+ Add Organization", on_click=_add_org_form).props("flat dense").classes("text-blue-600")

                    def _add_retention_form() -> None:
                        with ui.dialog() as d, ui.card().classes("p-6 gap-3 w-[420px]"):
                            ui.label("Register Artifact for Retention").classes("text-lg font-bold text-slate-800")
                            path_inp = ui.input("Artifact Path*", placeholder="/path/to/artifact.epub").classes("w-full")
                            days_inp = ui.number("Retention Days*", value=90, min=1).classes("w-full")

                            def _submit_ret() -> None:
                                path = path_inp.value.strip()
                                if not path:
                                    notify_error("Artifact path is required.")
                                    return
                                _retention.register_artifact(path, retention_days=int(days_inp.value or 90))
                                d.close()
                                notify_success(f"Artifact registered for {int(days_inp.value)} day retention.")
                                _render()

                            with ui.row().classes("justify-end gap-2 mt-2"):
                                ui.button("Cancel", on_click=d.close).props("flat").classes("text-slate-500")
                                ui.button("Register", on_click=_submit_ret).classes("bg-indigo-600 text-white")
                        d.open()

                    ui.button("+ Register Artifact", on_click=_add_retention_form).props("flat dense").classes("text-indigo-600")
                    ui.label(f"Organizations: {len(organizations)}").classes("text-xs text-slate-500")
                    ui.label(f"Memberships: {len(memberships)}").classes("text-xs text-slate-500")
                    ui.label(f"Artifacts tracked: {len(retention_records)}").classes("text-xs text-slate-500")

            with ui.card().classes("w-full p-5 rounded-xl border border-slate-200 mt-4"):
                ui.label("Audit & Compliance").classes("text-base font-semibold text-slate-700 mb-3")

                with ui.row().classes("gap-2 mb-2"):
                    def _export_provenance() -> None:
                        report = _compliance.generate_provenance_export()
                        _audit.record_event(
                            event_type="provenance_export_requested",
                            actor="user",
                            payload={"signature": report.get("signature")},
                        )
                        _render()

                    ui.button("Generate Provenance Export", on_click=_export_provenance).props("flat dense").classes(
                        "text-green-700"
                    )

                    def _export_governance() -> None:
                        report = _compliance.generate_governance_report()
                        _audit.record_event(
                            event_type="governance_report_requested",
                            actor="user",
                            payload={"signature": report.get("signature")},
                        )
                        _render()

                    ui.button("Generate Governance Report", on_click=_export_governance).props("flat dense").classes(
                        "text-amber-700"
                    )

                for event in audit_events[-8:]:
                    with ui.row().classes("items-center justify-between border-b border-slate-100 py-1"):
                        ui.label(f"{event['event_type']} ({event['actor']})").classes(
                            "text-sm text-slate-700"
                        )
                        ui.label(event["created_at"][:19]).classes("text-xs text-slate-400 font-mono")

    _render()

Pipelines page

Purpose: pipeline run controls and execution status tracking.

Pipeline orchestration page — executes multi-stage accessibility production pipelines.

Each pipeline run calls PipelineService.run_pipeline() which invokes each step via ExecutionService and persists results to DB via db.queries.

pipelines_page(content_area)

Render the Workflow Pipelines page.

Source code in accessibility_mgr/ui/pipelines.py
def pipelines_page(content_area: ui.element) -> None:
    """Render the Workflow Pipelines page."""
    content_area.clear()
    with content_area:
        section_header(
            "Workflow Pipelines",
            "Execute multi-stage accessibility production pipelines",
        )

        result_area = ui.column().classes("w-full gap-3 mt-2")

        ui.label("Available Pipelines").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-4 mb-2"
        )

        for pipeline in PipelineService.list_pipelines():
            _pipeline_card(pipeline, result_area)

        ui.label("Recent Pipeline Runs").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-8 mb-2"
        )
        recent_runs = Q.list_pipeline_runs(limit=10)
        if not recent_runs:
            ui.label("No pipeline runs recorded yet. Execute a pipeline above.").classes(
                "text-slate-400 text-sm"
            )
        else:
            with ui.card().classes("w-full rounded-xl border border-slate-200 overflow-hidden"):
                with ui.row().classes(
                    "px-4 py-2 bg-slate-50 text-xs font-semibold text-slate-500 "
                    "uppercase tracking-wider border-b"
                ):
                    ui.label("Pipeline").classes("flex-1")
                    ui.label("Status").classes("w-24")
                    ui.label("Started").classes("w-36")
                    ui.label("Finished").classes("w-36")
                for run in recent_runs:
                    ok = run.get("status") == "completed"
                    with ui.row().classes(
                        "items-center px-4 py-2 border-b border-slate-50 last:border-0 gap-2"
                    ):
                        ui.label(run.get("pipeline_name", "—")).classes(
                            "flex-1 text-sm text-slate-700"
                        )
                        ui.badge(run.get("status", "?")).classes(
                            f"w-24 text-center text-xs rounded "
                            f"{'bg-green-100 text-green-700' if ok else 'bg-red-100 text-red-700'}"
                        )
                        ui.label(str(run.get("started_at", ""))[:19]).classes(
                            "w-36 text-xs font-mono text-slate-400"
                        )
                        ui.label(str(run.get("finished_at", "") or "—")[:19]).classes(
                            "w-36 text-xs font-mono text-slate-400"
                        )

Purpose: 3-D print job management and workflow updates.

Print Jobs panel — log and manage 3-D print jobs with full workflow tracking.

Changes applied (see fix_specs.json): FIX-003 Metadata save now calls Q.log_event (persisted to DB). FIX-007 Job detail view added with five workflow step columns (designed, sliced, printed, inspected, delivered). FIX-016 Delivered step opens delivery confirmation dialog.

QA page

Purpose: QA execution and review interactions.

QA Tooling page — runs accessibility validation tools and shows history.

Changes applied (see fix_specs.json): FIX-012 Run dialog now has optional job-type selector and job-ID input. When a job is linked, the result appears in that job's event log.

qa_page(content_area)

Render the QA Tooling page.

Source code in accessibility_mgr/ui/qa.py
def qa_page(content_area: ui.element) -> None:
    """Render the QA Tooling page."""
    content_area.clear()
    with content_area:
        section_header(
            "Accessibility QA Tooling",
            "Run validation tools against accessibility-production assets",
        )

        result_area = ui.column().classes("w-full gap-3 mt-2")

        ui.label("Available Tools").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-4 mb-2"
        )
        for tool in QAService.list_tools():
            _tool_card(tool, result_area)

        ui.label("Recent QA Runs").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-8 mb-2"
        )
        recent = Q.list_qa_runs(limit=10)
        if not recent:
            ui.label("No QA runs recorded yet. Run a tool above.").classes(
                "text-slate-400 text-sm"
            )
        else:
            with ui.card().classes("w-full rounded-xl border border-slate-200 overflow-hidden"):
                with ui.row().classes(
                    "px-4 py-2 bg-slate-50 text-xs font-semibold text-slate-500 "
                    "uppercase tracking-wider border-b"
                ):
                    ui.label("Tool").classes("w-44")
                    ui.label("Linked Job").classes("w-36")
                    ui.label("Time").classes("w-36")
                    ui.label("Result").classes("w-20")
                    ui.label("Command").classes("flex-1")
                for run in recent:
                    ok = bool(run.get("success"))
                    with ui.row().classes(
                        "items-center px-4 py-2 border-b border-slate-50 last:border-0 gap-2"
                    ):
                        ui.label(run.get("tool_name", "—")).classes("w-44 text-sm")
                        if run.get("job_type") and run.get("job_id"):
                            ui.label(
                                f"{run['job_type']} #{run['job_id']}"
                            ).classes("w-36 text-xs font-mono text-indigo-600")
                        else:
                            ui.label("—").classes("w-36 text-xs text-slate-400")
                        ui.label(str(run.get("ran_at", ""))[:19]).classes(
                            "w-36 text-xs text-slate-400 font-mono"
                        )
                        ui.badge("✓ OK" if ok else "✗ FAIL").classes(
                            f"w-20 text-center text-xs rounded "
                            f"{'bg-green-100 text-green-700' if ok else 'bg-red-100 text-red-700'}"
                        )
                        ui.label(run.get("command", "")).classes(
                            "flex-1 text-xs font-mono text-slate-500 truncate"
                        )

QA dashboard

Purpose: QA metrics and historical quality trend visibility.

EPUB accessibility QA review page.

Runs a real DAISY Ace accessibility check against a user-supplied EPUB file, then opens a popup form pre-filled with the measures Ace reported (score, pass/fail, issues) so a reviewer can confirm or adjust them before submitting the result to the database.

AUDIT-FIX-002: this page previously ran against a hardcoded SAMPLE_EPUBS list through a self-documented "simulated" Ace check, and persisted results to an in-memory QAPersistenceService that was wiped on every restart. It now calls the real binary integration (services/epub_qa.py -> services/toolchain_binaries.py) and writes confirmed measures to the qa_measure table via db/queries.py.

qa_dashboard_page(content_area)

Render the EPUB accessibility QA review page.

Source code in accessibility_mgr/ui/qa_dashboard.py
def qa_dashboard_page(content_area: ui.element) -> None:
    """Render the EPUB accessibility QA review page."""
    content_area.clear()

    with content_area:
        section_header(
            "EPUB Accessibility QA Review",
            "Run DAISY Ace against a real EPUB, then review and submit the measures",
        )

        result_area = ui.column().classes("w-full gap-3 mt-2")

        with ui.card().classes("w-full p-5 rounded-xl border border-slate-200"):
            ui.label("Run Accessibility Check").classes(
                "text-lg font-semibold text-slate-700 mb-3"
            )

            epub_path_input = ui.input(
                "EPUB File Path",
                placeholder="/path/to/file.epub",
            ).classes("w-full")

            ui.separator().classes("my-2")
            ui.label("Link to Job (optional)").classes(
                "text-xs font-semibold text-slate-500 uppercase tracking-wider"
            )
            ui.label(
                "When linked, the submitted measure appears in that job's event log."
            ).classes("text-xs text-slate-400 mb-1")

            with ui.row().classes("gap-3 w-full"):
                job_type_sel = ui.select(
                    _JOB_TYPES, value="(none)", label="Job Type",
                ).classes("flex-1")
                job_id_inp = ui.input(
                    "Job ID", placeholder="numeric ID",
                ).classes("flex-1")

            def _run_check() -> None:
                epub_path = epub_path_input.value.strip()
                if not epub_path:
                    notify_error("Enter an EPUB file path first.")
                    return

                job_type_val = (
                    job_type_sel.value if job_type_sel.value != "(none)" else None
                )
                job_id_raw = job_id_inp.value.strip()
                job_id_val = (
                    int(job_id_raw)
                    if job_type_val and job_id_raw.isdigit()
                    else None
                )

                result_area.clear()
                with result_area:
                    with ui.card().classes(
                        "p-4 rounded-xl border border-slate-200 w-full"
                    ):
                        ui.label(f"Running DAISY Ace against {epub_path}…").classes(
                            "text-slate-600 font-medium"
                        )
                        ui.spinner("dots", size="sm")

                def _do() -> None:
                    # asset_id is only used as an in-process dict key inside
                    # EPUBQAService; the database link is job_type/job_id.
                    result = _qa_service.run_ace_check(
                        asset_id=job_id_val or 0,
                        epub_path=epub_path,
                    )
                    result_area.clear()
                    with result_area:
                        _render_result_card(result, epub_path)
                    _open_measure_dialog(
                        result, epub_path, job_type_val, job_id_val, measures_box
                    )

                threading.Thread(target=_do, daemon=True).start()

            ui.button("▶ Run Ace Check", on_click=_run_check).classes(
                "bg-blue-600 text-white mt-4"
            )

        ui.label("Submitted QA Measures").classes(
            "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-8 mb-2"
        )
        measures_box = ui.column().classes("w-full gap-2")
        _render_measures_box(measures_box)

Reports page

Purpose: faceted reporting and export of jobs across workflow types.

Reports page — faceted filtering and export of jobs by student, school, grade, material type, status, and date range.

New file (FIX-015).

reports_page(content_area, presets=None)

Render the Reports page.

Source code in accessibility_mgr/ui/reports.py
def reports_page(content_area: ui.element, presets: dict[str, str] | None = None) -> None:
    """Render the Reports page."""
    presets = presets or {}
    content_area.clear()
    with content_area:
        section_header(
            "Reports",
            "Filter and export jobs by school, grade, material type, and status",
        )

        # ── Filter controls ───────────────────────────────────────────────────
        with ui.card().classes("w-full p-5 rounded-xl border border-slate-200 mb-4"):
            ui.label("Filters").classes(
                "text-xs font-semibold text-slate-500 uppercase tracking-wider mb-3"
            )
            with ui.grid(columns=3).classes("gap-4 w-full"):
                school_inp = ui.input(
                    "School", placeholder="e.g. Legacy Jr. High"
                ).classes("w-full").props("outlined dense clearable")

                grade_inp = ui.input(
                    "Grade", placeholder="e.g. 7"
                ).classes("w-full").props("outlined dense clearable")

                type_sel = ui.select(
                    {v: l for v, l in _TYPE_OPTIONS},
                    value=presets.get("job_type", ""),
                    label="Material Type",
                ).classes("w-full").props("outlined dense")

                status_sel = ui.select(
                    {v: l for v, l in _STATUS_OPTIONS},
                    value=presets.get("status", "all"),
                    label="Status",
                ).classes("w-full").props("outlined dense")

                priority_sel = ui.select(
                    {v: l for v, l in _PRIORITY_OPTIONS},
                    value=presets.get("priority", "all"),
                    label="Priority",
                ).classes("w-full").props("outlined dense")

                date_from_inp = ui.input(
                    "Created From (YYYY-MM-DD)", placeholder=str(date.today())
                ).classes("w-full").props("outlined dense clearable")

                date_to_inp = ui.input(
                    "Created To (YYYY-MM-DD)", placeholder=str(date.today())
                ).classes("w-full").props("outlined dense clearable")

            # Student selector
            students = Q.list_students()
            student_map = {"": "All Students"}
            for s in students:
                label = f"{s['last_name']}, {s['first_name']}"
                if s.get("school"):
                    label += f" — {s['school']}"
                student_map[str(s["id"])] = label

            student_sel = ui.select(
                student_map,
                value="",
                label="Student",
            ).classes("w-full mt-2").props("outlined dense")

            with ui.row().classes("gap-3 mt-4 items-center"):
                run_btn = ui.button("Run Report").classes(
                    "bg-blue-600 text-white rounded-lg px-4 py-2"
                )
                export_btn = ui.button("Export CSV").props("flat").classes(
                    "text-blue-600 border border-blue-300 rounded-lg px-4 py-2"
                )
                results_label = ui.label("").classes("text-sm text-slate-400 ml-2")

        # ── Results area ──────────────────────────────────────────────────────
        results_area = ui.column().classes("w-full gap-4")

        # Store last result for CSV export
        _last_result: dict = {}

        def _run_report() -> None:
            nonlocal _last_result
            results_area.clear()

            sid_str = student_sel.value
            sid = int(sid_str) if sid_str and sid_str.isdigit() else None
            status_val = status_sel.value if status_sel.value != "all" else None
            priority_val = priority_sel.value if priority_sel.value != "all" else None
            type_val = type_sel.value or None
            df = date_from_inp.value.strip() or None
            dt = date_to_inp.value.strip() or None

            if df and not validate_iso_date(df, "Created From"):
                return
            if dt and not validate_iso_date(dt, "Created To"):
                return
            if df and dt and df > dt:
                ui.notify(
                    "Created From must be earlier than or equal to Created To",
                    type="negative",
                    position="top-right",
                )
                return

            result = Q.report_jobs(
                school=school_inp.value.strip() or None,
                grade=grade_inp.value.strip() or None,
                job_type=type_val,
                status=status_val,
                priority=priority_val,
                date_from=df,
                date_to=dt,
                student_id=sid,
            )
            _last_result = result

            total = result["total_jobs"]
            by_type = result["by_type"]

            results_label.set_text(
                f"{total} job(s) found"
                + (
                    " — " + ", ".join(f"{_TYPE_LABELS.get(k, k)}: {v}" for k, v in by_type.items() if v)
                    if total else ""
                )
            )

            if total == 0:
                with results_area:
                    with ui.card().classes(
                        "p-8 text-center border border-slate-200 rounded-xl w-full"
                    ):
                        ui.label("No jobs match the selected filters.").classes(
                            "text-slate-400"
                        )
                return

            # ── Summary cards ─────────────────────────────────────────────────
            with results_area:
                with ui.row().classes("gap-3 flex-wrap mb-2"):
                    for jtype, count in by_type.items():
                        if not count:
                            continue
                        color_map = {
                            "braille":     ("bg-indigo-50", "text-indigo-600", "border-indigo-200"),
                            "lp_ebraille": ("bg-green-50",  "text-green-600",  "border-green-200"),
                            "tactile":     ("bg-rose-50",   "text-rose-600",   "border-rose-200"),
                            "print":       ("bg-amber-50",  "text-amber-600",  "border-amber-200"),
                        }
                        bg, text, border = color_map.get(jtype, ("bg-slate-50", "text-slate-600", "border-slate-200"))
                        with ui.card().classes(f"p-4 {bg} border {border} rounded-xl shadow-sm"):
                            ui.label(str(count)).classes(f"text-3xl font-bold {text}")
                            ui.label(_TYPE_LABELS.get(jtype, jtype)).classes(
                                "text-slate-600 text-sm mt-1"
                            )

                # ── Jobs table ────────────────────────────────────────────────
                with ui.card().classes(
                    "w-full rounded-xl border border-slate-200 overflow-hidden"
                ):
                    with ui.row().classes(
                        "px-4 py-2 bg-slate-50 text-xs font-semibold text-slate-500 "
                        "uppercase tracking-wider border-b gap-2"
                    ):
                        ui.label("Title / Object").classes("flex-1")
                        ui.label("Type").classes("w-36")
                        ui.label("Student / Requester").classes("w-44")
                        ui.label("School").classes("w-36")
                        ui.label("Grade").classes("w-16 text-center")
                        ui.label("Priority").classes("w-20 text-center")
                        ui.label("Status").classes("w-24 text-center")
                        ui.label("Created").classes("w-24")

                    for job in result["jobs"]:
                        status = job.get("status", "not_started")
                        status_color = {
                            "delivered":   "bg-green-100 text-green-700",
                            "in_progress": "bg-blue-100 text-blue-700",
                            "not_started": "bg-slate-100 text-slate-500",
                        }.get(status, "bg-slate-100 text-slate-500")
                        status_label = status.replace("_", " ").title()

                        requester = ""
                        if job.get("last_name"):
                            requester = f"{job['last_name']}, {job['first_name']}"
                        elif job.get("requester"):
                            requester = job["requester"]

                        with ui.row().classes(
                            "items-center px-4 py-3 border-b border-slate-50 "
                            "last:border-0 hover:bg-slate-50 gap-2"
                        ):
                            ui.label(job.get("title") or "—").classes(
                                "flex-1 text-sm text-slate-700 truncate"
                            )
                            ui.badge(
                                _TYPE_LABELS.get(job.get("job_type", ""), "—")
                            ).classes(
                                "w-36 text-xs rounded px-1 "
                                + {
                                    "braille":     "bg-indigo-50 text-indigo-700",
                                    "lp_ebraille": "bg-green-50 text-green-700",
                                    "large_print": "bg-green-50 text-green-700",
                                    "ebraille":    "bg-green-50 text-green-700",
                                    "epub3_daisy": "bg-green-50 text-green-700",
                                    "tactile":     "bg-rose-50 text-rose-700",
                                    "print":       "bg-amber-50 text-amber-700",
                                }.get(job.get("job_type", ""), "bg-slate-100 text-slate-600")
                            )
                            ui.label(requester or "—").classes(
                                "w-44 text-xs text-slate-600 truncate"
                            )
                            ui.label(job.get("school") or "—").classes(
                                "w-36 text-xs text-slate-500 truncate"
                            )
                            ui.label(job.get("grade") or "—").classes(
                                "w-16 text-xs text-center text-slate-500"
                            )
                            with ui.element("div").classes("w-20 flex justify-center"):
                                priority_badge(job.get("priority") or "normal")
                            ui.badge(status_label).classes(
                                f"w-24 text-center text-xs rounded {status_color}"
                            )
                            ui.label(str(job.get("created_at", ""))[:10]).classes(
                                "w-24 text-xs text-slate-400 font-mono"
                            )

        def _export_csv() -> None:
            if not _last_result:
                ui.notify("Run a report first.", type="warning", position="top-right")
                return

            jobs = _last_result.get("jobs", [])
            if not jobs:
                ui.notify("No results to export.", type="warning", position="top-right")
                return

            output = io.StringIO()
            writer = csv.DictWriter(
                output,
                fieldnames=[
                    "id", "title", "job_type", "requester",
                    "last_name", "first_name", "school", "grade",
                    "priority", "status", "created_at",
                ],
                extrasaction="ignore",
            )
            writer.writeheader()
            for job in jobs:
                writer.writerow(job)

            csv_bytes = output.getvalue().encode("utf-8")
            filename = f"accessibility_report_{date.today().isoformat()}.csv"
            ui.download(csv_bytes, filename)

        run_btn.on("click", lambda: _run_report())
        export_btn.on("click", lambda: _export_csv())

        if presets:
            _run_report()

Search page

Purpose: cross-entity search UI over jobs, files, metadata, and events.

Search page — full-text search across jobs, files, metadata, and event log.

Changes applied (see fix_specs.json): FIX-009 Replaced in-memory Python filtering with Q.search_all() which uses parameterised SQL LIKE queries — no more per-job DB round-trips. FIX-014 Event log content, agent names, and file checksums are now searchable.

search_page(content_area)

Render the Search page.

Source code in accessibility_mgr/ui/search.py
def search_page(content_area: ui.element) -> None:
    """Render the Search page."""
    content_area.clear()
    with content_area:
        section_header("Search", "Find jobs, files, metadata, and events across the system")

        with ui.row().classes("gap-3 items-center w-full"):
            query_inp = ui.input(
                placeholder="Title, requester, filename, metadata value, SHA-256 hash…"
            ).classes("flex-1").props("outlined clearable")
            ui.button("Search", on_click=lambda: _execute()).classes(
                "bg-blue-600 text-white rounded-lg px-4 py-2"
            )

        results_area = ui.column().classes("w-full gap-4 mt-4")

        def _execute() -> None:
            q = (query_inp.value or "").strip()
            if not q:
                results_area.clear()
                return

            results_area.clear()
            # FIX-009: single call to SQL-backed search function
            data = Q.search_all(q)

            total = sum(len(v) for v in data.values())

            with results_area:
                ui.label(f"{total} result(s) for \u201c{q}\u201d").classes(
                    "text-slate-500 text-sm"
                )

                if not total:
                    ui.label("No matches found.").classes("text-slate-400 text-sm mt-2")
                    return

                def _section(title: str, items: list, render_fn) -> None:
                    if not items:
                        return
                    ui.label(title).classes(
                        "text-sm font-semibold text-slate-500 uppercase tracking-wider mt-4 mb-2"
                    )
                    with ui.card().classes(
                        "w-full rounded-xl border border-slate-200 overflow-hidden"
                    ):
                        for item in items:
                            render_fn(item)

                # ── Braille jobs ──────────────────────────────────────────────
                def _braille_row(j: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        with ui.column().classes("flex-1 gap-0"):
                            ui.label(j["title"]).classes("text-sm font-medium text-slate-700")
                            ui.label(
                                f"{j.get('braille_type', '').capitalize()} · "
                                f"{j.get('requester') or '—'} · {j.get('priority', '')}"
                            ).classes("text-xs text-slate-400")
                        ui.label(str(j.get("created_at", ""))[:10]).classes(
                            "text-xs text-slate-400 font-mono"
                        )

                _section("Braille Jobs", data["braille_jobs"], _braille_row)

                # ── LP / eBraille / EPUB3 jobs ────────────────────────────────
                def _lp_row(j: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        with ui.column().classes("flex-1 gap-0"):
                            ui.label(j["title"]).classes("text-sm font-medium text-slate-700")
                            ui.label(
                                f"{j.get('job_type', '').replace('_', ' ').title()} · "
                                f"{j.get('requester') or '—'}"
                            ).classes("text-xs text-slate-400")
                        ui.label(str(j.get("created_at", ""))[:10]).classes(
                            "text-xs text-slate-400 font-mono"
                        )

                _section("Large Print / eBraille / EPUB3 Jobs", data["lp_jobs"], _lp_row)

                # ── Tactile jobs ──────────────────────────────────────────────
                def _tactile_row(j: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        with ui.column().classes("flex-1 gap-0"):
                            ui.label(j["title"]).classes("text-sm font-medium text-slate-700")
                            ui.label(
                                f"{j.get('tactile_type', '').replace('_', ' ').title()} · "
                                f"{j.get('requester') or '—'}"
                            ).classes("text-xs text-slate-400")
                        ui.label(str(j.get("created_at", ""))[:10]).classes(
                            "text-xs text-slate-400 font-mono"
                        )

                _section("Tactile Graphics Jobs", data["tactile_jobs"], _tactile_row)

                # ── Print jobs ────────────────────────────────────────────────
                def _print_row(j: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        ui.label(
                            j.get("object_name") or j.get("file_name") or "—"
                        ).classes("flex-1 text-sm text-slate-700")
                        ui.label(j.get("requester") or "—").classes("text-xs text-slate-400")
                        ui.badge(
                            "✓ OK" if j.get("successful") else "✗ FAIL"
                        ).classes(
                            "text-xs rounded "
                            + ("bg-green-100 text-green-700" if j.get("successful")
                               else "bg-red-100 text-red-700")
                        )

                _section("3-D Print Jobs", data["print_jobs"], _print_row)

                # ── Students ──────────────────────────────────────────────────
                def _student_row(s: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        ui.label(
                            f"{s.get('last_name', '')}, {s.get('first_name', '')}"
                        ).classes("flex-1 text-sm font-medium text-slate-700")
                        ui.label(s.get("school") or "—").classes("text-xs text-slate-400")
                        ui.label(f"Grade {s.get('grade', '—')}").classes(
                            "text-xs text-slate-400"
                        )

                _section("Students", data["students"], _student_row)

                # ── Files ─────────────────────────────────────────────────────
                def _file_row(f: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        with ui.column().classes("flex-1 gap-0 min-w-0"):
                            ui.label(f["original_name"]).classes(
                                "text-sm text-slate-700 truncate"
                            )
                            chk = f.get("checksum_sha256") or ""
                            if chk:
                                ui.label(chk[:16] + "…").classes(
                                    "text-xs font-mono text-slate-400"
                                ).tooltip(chk)
                        ui.label(f.get("file_use") or "—").classes("text-xs text-slate-400 w-24")
                        ui.label(f.get("format_name") or "—").classes("text-xs text-slate-400 w-20")
                        ui.label(str(f.get("created_at", ""))[:10]).classes(
                            "text-xs text-slate-400 font-mono w-24"
                        )

                _section("Files", data["files"], _file_row)

                # ── Metadata ──────────────────────────────────────────────────
                def _meta_row(m: dict) -> None:
                    with ui.row().classes(
                        "items-center px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        ui.label(
                            f"{m['job_type']} #{m['job_id']}"
                        ).classes("text-xs text-slate-400 w-28 shrink-0")
                        ui.label(m["meta_key"]).classes(
                            "text-xs font-mono text-slate-500 w-44 shrink-0"
                        )
                        ui.label(m["meta_value"]).classes("text-sm text-slate-700 flex-1")

                _section("Metadata", data["metadata"], _meta_row)

                # ── Event log (FIX-014) ───────────────────────────────────────
                def _event_row(ev: dict) -> None:
                    outcome_color = {
                        "SUCCESS": "text-green-600",
                        "FAILURE": "text-red-600",
                        "WARNING": "text-amber-600",
                    }.get(ev.get("event_type", ""), "text-slate-600")
                    with ui.row().classes(
                        "items-start px-4 py-3 border-b border-slate-50 last:border-0 gap-3"
                    ):
                        ui.label(str(ev.get("event_datetime", ""))[:19]).classes(
                            "text-xs text-slate-400 font-mono w-36 shrink-0"
                        )
                        ui.label(
                            f"{ev.get('job_type', '')} #{ev.get('job_id', '')}"
                        ).classes("text-xs text-slate-400 w-24 shrink-0")
                        ui.badge(ev.get("event_type", "")).classes(
                            "text-xs bg-slate-100 text-slate-700 rounded px-1 shrink-0"
                        )
                        ui.label(ev.get("agent") or "system").classes(
                            "text-xs text-slate-400 italic w-20 shrink-0"
                        )
                        ui.label(ev.get("detail") or "").classes(
                            f"text-sm {outcome_color} flex-1 break-words"
                        )

                _section("Event Log", data["events"], _event_row)

        query_inp.on("keydown.enter", lambda _: _execute())

Security dashboard

Purpose: security posture views and access-related controls.

RBAC security and authorization dashboard.

security_dashboard_page(content_area)

Render RBAC security dashboard.

Source code in accessibility_mgr/ui/security_dashboard.py
def security_dashboard_page(content_area: ui.element) -> None:
    """Render RBAC security dashboard."""

    content_area.clear()

    with content_area:
        section_header(
            "Security & Authorization",
            "Reference implementation — RBAC is not yet enforced on live sessions",
        )

        with ui.grid(columns=2).classes("w-full gap-4 mb-6"):
            with ui.card().classes(
                "p-5 rounded-xl border border-slate-200"
            ):
                ui.label("Registered Roles").classes(
                    "text-sm text-slate-500"
                )
                ui.label(str(len(_rbac.list_roles()))).classes(
                    "text-3xl font-bold text-slate-700"
                )

            with ui.card().classes(
                "p-5 rounded-xl border border-slate-200"
            ):
                ui.label("Authorization Checks").classes(
                    "text-sm text-slate-500"
                )
                ui.label("Simulated Only").classes(
                    "text-3xl font-bold text-amber-600"
                )

        with ui.card().classes(
            "w-full p-5 rounded-xl border border-slate-200 mb-6"
        ):
            ui.label("Role Definitions").classes(
                "text-base font-semibold text-slate-700 mb-3"
            )

            for role in _rbac.list_roles():
                with ui.column().classes(
                    "border-b border-slate-100 py-3 gap-1"
                ):
                    ui.label(role["name"]).classes(
                        "text-sm font-semibold text-slate-700"
                    )

                    for permission in role["permissions"]:
                        ui.badge(permission).classes(
                            "bg-slate-100 text-slate-700 mr-1"
                        )

        with ui.card().classes(
            "w-full p-5 rounded-xl border border-slate-200"
        ):
            ui.label("Authorization Simulation").classes(
                "text-base font-semibold text-slate-700 mb-3"
            )

            checks = [
                (
                    _admin.username,
                    "governance.manage",
                    _rbac.authorize(_admin, "governance.manage"),
                ),
                (
                    _operator.username,
                    "rbac.manage",
                    _rbac.authorize(_operator, "rbac.manage"),
                ),
            ]

            for username, permission, result in checks:
                with ui.row().classes(
                    "items-center justify-between border-b border-slate-100 py-2"
                ):
                    ui.label(f"{username}{permission}").classes(
                        "text-sm text-slate-700"
                    )

                    ui.badge(
                        "authorized" if result else "denied"
                    ).classes(
                        "bg-green-100 text-green-700"
                        if result
                        else "bg-red-100 text-red-700"
                    )

Tactile graphics page

Purpose: tactile graphics job management and step progression.

Tactile graphics jobs panel.

Changes applied (see fix_specs.json): FIX-006 _ingest_dialog added; Files card added to job detail view. FIX-003 _save_all in metadata dialog now calls Q.log_event (persisted to DB). FIX-016 Delivered step opens delivery confirmation dialog instead of direct toggle.

Workflow monitor

Purpose: queued workflow execution monitoring and state transitions.

Workflow execution monitoring dashboard.

workflow_monitor_page(content_area)

Render workflow execution monitoring UI.

Source code in accessibility_mgr/ui/workflow_monitor.py
def workflow_monitor_page(content_area: ui.element) -> None:
    """Render workflow execution monitoring UI."""
    _ensure_runtime_started()
    content_area.clear()

    with content_area:
        section_header(
            "Workflow Monitor",
            "Background orchestration and execution monitoring",
        )

        with ui.card().classes(
            "w-full p-5 rounded-xl border border-slate-200 mb-6"
        ):
            ui.label("Queue State").classes(
                "text-base font-semibold text-slate-700 mb-3"
            )

            for job in _queue.list_jobs():
                with ui.row().classes(
                    "items-center justify-between border-b border-slate-100 py-2"
                ):
                    with ui.column().classes("gap-0"):
                        ui.label(job["workflow_name"]).classes(
                            "text-sm font-medium text-slate-700"
                        )

                        ui.label(
                            f"Asset #{job['asset_id']}"
                        ).classes("text-xs text-slate-500")

                    ui.badge(job["status"]).classes(
                        "bg-slate-100 text-slate-700"
                    )

        with ui.card().classes(
            "w-full p-5 rounded-xl border border-slate-200"
        ):
            ui.label("Worker Executions").classes(
                "text-base font-semibold text-slate-700 mb-3"
            )

            for execution in _runtime.list_executions():
                with ui.row().classes(
                    "items-start justify-between border-b border-slate-100 py-2 gap-3"
                ):
                    with ui.column().classes("gap-0 flex-1"):
                        ui.label(execution["workflow_name"]).classes(
                            "text-sm font-medium text-slate-700"
                        )

                        ui.label(
                            f"Worker: {execution['worker_name']}"
                        ).classes("text-xs text-slate-500")

                        ui.label(
                            f"Asset #{execution['asset_id']}"
                        ).classes("text-xs text-slate-500")

                    ui.label(execution["status"]).classes(
                        "text-sm font-semibold text-green-600"
                    )

                    ui.label(execution["started_at"][:19]).classes(
                        "text-xs text-slate-400 font-mono"
                    )