Plan: Job History Panel + Per-Row Sync Progress¶
TL;DR: Extend the Pinia store to record all SSE updates per job in-memory. Show a collapsible "Job History" panel below the import table with full per-job detail (stats, row errors, raw JSON). Add an inline indeterminate progress bar per table row while that row's sync is active.
Phase 1 — Store (backofficeDataManagement.ts)¶
- Add
JobHistoryEntryinterface:interface JobHistoryEntry { jobId: number; labelKey: string; // i18n key for module name year: number; targetType: "data_entries" | "factors"; moduleTypeId: number; startedAt: Date; completedAt?: Date; status: "in_progress" | "completed" | "failed"; allUpdates: JobUpdatePayload[]; latestPayload?: JobUpdatePayload; } - Add
jobHistory = ref<JobHistoryEntry[]>([])reactive state - Update
subscribeToJobUpdatessignature with optional 4th param:
jobMeta?: { labelKey: string; moduleTypeId: number; year: number; targetType: 'data_entries' | 'factors' }
- On subscribe: push a new
JobHistoryEntrywithstatus: 'in_progress'tojobHistory - On each SSE message: find entry by
jobId, push toallUpdates, updatelatestPayload - On COMPLETED (status=3): set
completedAt, setstatus = 'completed' -
On FAILED (status=4): set
completedAt, setstatus = 'failed' -
Add
clearJobHistory()action:jobHistory.value = [] - Add
getActiveJobForRow(moduleTypeId: number, year: number, targetType: 'data_entries' | 'factors'): JobHistoryEntry | undefined— looks for the first entry withstatus === 'in_progress'matching the three keys; used to drive per-row UI binding - Expose all new state/actions in the store return
Phase 2 — New component JobHistoryPanel.vue¶
Create frontend/src/components/organisms/data-management/JobHistoryPanel.vue
- Reads
dataManagementStore.jobHistorydirectly (no props needed) - "Clear history" button → calls
dataManagementStore.clearJobHistory() - Empty state when
jobHistory.length === 0 - Each entry rendered as
q-expansion-item: - Header:
$t(entry.labelKey), year, target type badge (data_management_job_target_data_entries/_target_factors), status badge (color: warning=in_progress, positive=completed, negative=failed), formattedstartedAttimestamp - Body:
- Stats row:
rows_processed,rows_skipped,row_errors_countfromlatestPayload?.meta row_errorslist: "Row N: reason" for each error inlatestPayload?.meta?.row_errors- "Show raw JSON" toggle →
<pre>block withJSON.stringify(latestPayload, null, 2)
- Stats row:
Phase 3 — AnnualDataImport.vue updates¶
- Add
uploadLabelKey = ref<string | null>(null)local ref - Set
uploadLabelKey.value = row.labelKeyin bothopenUploadCsvDialogandopenUploadFactorsDialog - Pass
jobMeta(4th arg) tosubscribeToJobUpdatescall inonFilesUploaded:dataManagementStore.subscribeToJobUpdates(jobId, onCompleted, onFail, { labelKey: uploadLabelKey.value!, moduleTypeId: module_type_id, year, targetType: uploadTargetType.value, }); - Add inline per-row progress in both Data and Factors cells:
- Compute
activeJob = dataManagementStore.getActiveJobForRow(row.moduleTypeId, year, targetType) - If
activeJobis defined → showq-linear-progress indeterminate+activeJob.latestPayload?.status_messagetext below the upload buttons - Once
activeJobclears (sync done), spinner disappears; history panel handles the final state
- Compute
- Add
<job-history-panel />inside aq-expansion-itemlabeled$t('data_management_job_history')below theq-markup-table
Phase 4 — i18n (backoffice_data_management.ts)¶
Add the following keys (EN + FR) to frontend/src/i18n/en-US/backoffice_data_management.ts:
| Key | EN | FR |
|---|---|---|
data_management_job_history | Job History | Historique des imports |
data_management_job_history_empty | No import jobs in this session. | Aucun import dans cette session. |
data_management_job_history_clear | Clear history | Effacer l'historique |
data_management_job_status_in_progress | In progress | En cours |
data_management_job_status_completed | Completed | Terminé |
data_management_job_status_failed | Failed | Échoué |
data_management_job_rows_processed | Rows processed | Lignes traitées |
data_management_job_rows_skipped | Rows skipped | Lignes ignorées |
data_management_job_row_errors | Row errors | Erreurs de ligne |
data_management_job_show_raw | Show raw JSON | Afficher le JSON brut |
data_management_job_hide_raw | Hide raw JSON | Masquer le JSON brut |
data_management_job_target_data_entries | Data entries | Données |
data_management_job_target_factors | Factors | Facteurs |
data_management_job_sync_in_progress | Sync in progress… | Synchronisation en cours… |
Relevant Files¶
frontend/src/stores/backofficeDataManagement.ts— AddJobHistoryEntry,jobHistory,getActiveJobForRow,clearJobHistory; updatesubscribeToJobUpdatesfrontend/src/components/organisms/data-management/JobHistoryPanel.vue— NEW componentfrontend/src/components/organisms/data-management/AnnualDataImport.vue— per-row progress + history panelfrontend/src/i18n/en-US/backoffice_data_management.ts— new i18n keys
Verification¶
- Upload a CSV → inline spinner appears on that row while SSE stream is active
- Sync completes → spinner disappears, history panel adds entry with green badge + stats
- Sync fails → red entry with row errors list + raw JSON visible
- "Clear history" empties the panel
make lint(frontend) passes with no TS/ESLint errors
Decisions¶
- History is in-memory only (Pinia store), cleared on page refresh
- History panel is a collapsible section below the import table
- Progress is per-row inline (next to upload buttons), indeterminate only (backend doesn't emit percentage)
- No localStorage/sessionStorage persistence
Out of Scope¶
- Persisting history across page refreshes
- Progress percentage (backend doesn't emit one)
- History for API-triggered syncs (same mechanism applies but not specifically tested)