You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The wizard exposes Type ID even though it is not a real job type. The same user input is used to derive the job key, archive prefix, filenames, cache paths, log correlation, status grouping, and joins across dependent stores. The system also appends -backup without making the complete stored archive prefix an explicit user-owned value.
Changing Type ID or location therefore looks like deleting one job and creating another to parts of the application. This causes stale or duplicated dashboard entries and can disconnect historical status, reports, restore tests, schedules, notifications, widgets, repository assignments, and restore history from the logical job the user edited.
Introduce a generated immutable job_id, remove Type ID from the new user-facing and canonical job model, and make the complete archive prefix explicit. Use job_id as the only canonical foreign key across every job-dependent subsystem.
This is an application-wide architecture migration, not a dashboard-only fix.
Current problem
Today different areas infer job identity or behavior from one or more mutable values:
job metadata filename and job_key,
backup_type / Type ID,
location,
status and log filenames,
archive prefixes derived by appending -backup,
repository used_by / source_job_keys,
schedule map keys,
restore-test filenames,
runtime control paths and locks.
For example, changing paperless_storagebox to documents_storagebox removes the old metadata key but keeps historical paperless_storagebox status files. The dashboard currently starts with all status keys and only adds missing configured jobs, so both identities may be displayed even though only one configured job exists.
The UI model is also unclear:
Job name: user-facing display label
Type ID: technical fragment with several hidden consequences
Location: repository/storage property and part of the inferred identity
A user entering paperless does not directly own or see every value derived from it.
Goals
Give every job a generated UUID that never changes during the lifetime of that job.
Use job_id as the only canonical foreign key between job-dependent stores and runtime services.
Replace the user-facing Type ID with an explicit complete archive prefix.
Do not append hidden text to the archive prefix entered or accepted by the user.
Keep the job name, archive prefix, repository assignment, icon, description, sources, and operational settings editable.
Derive location from the selected repository and its storage target instead of using it as identity.
Preserve the existing archive_prefixes behavior: the current prefix is first and previous prefixes remain available for restore and retention scoping.
Preserve historical descriptor values as run-time snapshots without using them as identity.
Keep exactly one dashboard/widget entry per configured job.
Preserve reports, history, restore tests, restore runs, and archive access across job name and archive-prefix changes within the current repository.
Migrate existing installations once, atomically, idempotently, and with a complete audit trail.
Preserve ambiguous legacy data without guessing or silently linking it to the wrong job.
Non-goals
Do not replace readable Borg archive prefixes with UUIDs.
Do not use UUIDs as authentication tokens or secrets.
Do not delete old status, report, restore-test, restore-history, or archive data merely because it cannot be linked automatically.
Do not keep two permanent internal identity systems after migration.
Do not add repository assignment history.
Do not search multiple current or historical repositories for one job.
Do not move or copy archives when a repository assignment changes.
Do not change repository deletion rules as part of this issue.
Do not silently merge jobs because their names, prefixes, repositories, or legacy keys match.
The user-safety warning and confirmation for changing a job repository are tracked separately in #452.
job_id is generated once with UUIDv4 and is immutable.
The metadata filename is <job_id>.json.
New jobs do not use job_key, backup_type, Type ID, or location as identity or canonical active metadata.
name is the editable display label.
repository_key is the one current repository assignment.
archive_prefixes is an ordered, de-duplicated list.
archive_prefixes[0] is the complete current prefix used for new archives.
Remaining archive_prefixes entries are previous complete prefixes used to find and manage existing archives in the current repository.
The wizard displays and edits the complete current prefix exactly as stored. It does not append -backup or another hidden suffix.
A new job may receive an editable archive-prefix suggestion derived from its display name, but the preview must show the exact resulting archive pattern before save.
When the current prefix changes, the previous prefix is retained in archive_prefixes.
legacy_job_keys exists only for migration, legacy-data resolution, and a bounded API compatibility period. It is never a new active foreign key.
Duplicating or importing a job as a new job always creates a new job_id.
Updating an existing job retains its job_id.
Location is resolved from the current repository and storage context. Historical records may retain the location used for that run as a snapshot.
Snapshot fields do not need an _at_run suffix because a run record is inherently historical. Legacy records may retain backup_type and job_key, but new records and readers must not use them as canonical identity.
Status and log filenames remain readable for administrators. Use the archive prefix or a bounded display slug captured for the run plus a short UUID suffix, for example:
The complete UUID remains in the JSON payload, runtime state, and structured log context. The short suffix is only a human-friendly correlation aid and is not used as identity. Readers use payload or persisted run metadata as the source of truth and never infer canonical identity from a readable filename.
Repository assignment boundary
A job has exactly one current repository_key.
When the repository assignment changes:
job_id remains unchanged.
The old current assignment is removed atomically.
The new current assignment is added atomically.
Future backups use only the new repository.
Existing archives are not moved or copied.
Browse & Restore for the job uses only the current repository.
Historical run records may display the repository used at that time as a snapshot.
No repository_history is added to the job.
The dedicated warning and explicit confirmation are implemented separately under #452.
Dependency inventory and required target behavior
Area
Current dependency
Required behavior with job_id
Job metadata and discovery
Metadata filename, job_key, backup_type, and location
Store and discover by immutable job_id; expose name, archive prefixes, and current repository separately
Wizard create/edit
Type ID and location derive identity and archive prefix
Create UUID once; edits retain it; edit the complete current archive prefix explicitly
Jobs API and UI actions
Run, cancel, enable, edit, delete, and live log use job_key
Address actions by job_id; return readable name and prefix separately
Dashboard
Starts from latest .status per mutable key and supplements configured jobs
Start from configured jobs by job_id; overlay latest run, runtime state, schedule, restore proof, and repository details
Unraid dashboard widget
Joins job/status/cache data by current key
Cache and aggregate by job_id; one widget row/count per configured job
Homepage widget API
Reads metadata/status/schedules by job_key
Use job_id internally and return readable descriptors for display
Backup status
Filename and payload use Type ID plus location
Write job_id; keep name, archive prefix, repository, and location as run snapshots
Weekly repository snapshots
Maps sizes by mutable status key
Key snapshots by job_id; preserve historical descriptor metadata where useful
History
Parses Type ID/location from status filenames
Read status payload and group/filter by job_id; support unassigned legacy records
Reports
Discovers report jobs and time series from filenames and parsed job_key
Select and aggregate by job_id; name/prefix changes remain one continuous report
Report mail
Matches planned jobs, metadata, schedules, and statuses by key
Join by job_id; intentionally display current job labels and historical run snapshots
Browse and Restore
Uses job_key to resolve repository and archive-prefix filters
Select by job_id; resolve the current repository and ordered archive_prefixes
Archive naming and retention
Prefix is derived from Type ID
Use the explicit current archive_prefixes[0]; keep readable historical prefixes for the current repository
Restore precheck and execution
Request and persisted run contain job_key
Store job_id plus run-time job, repository, prefix, and archive snapshots
Restore history
Summary/detail/index store job_key
Store and filter by job_id; keep historical display descriptors
Restore tests
<job_key>.test, policies, and plan rows use key
Use <job_id>.test; result payload, policy, scheduling, reminders, and proof map use job_id
Schedules and cron
schedules.json is keyed by job_key; cron posts job_key
Key schedules by job_id; cron posts job_id; readable descriptors remain diagnostic only
Repository assignment
Repository used_by and source_job_keys contain mutable keys
Replace with job_ids / source_job_ids; derive display labels from current job inventory
Repository reconciliation
Compares repository links to metadata job_key
Reconcile immutable IDs and report missing IDs explicitly
Repository maintenance
Job lookup and archive filtering use mutable key or derived prefix
Resolve by ID, then use current repository and explicit prefix list
Runtime manager
In-memory state and subprocess environment are keyed by job_key
Key runtime state by job_id; include readable values only in logs and snapshots
Cancellation control
Runtime paths, control state, and validation use key
Stable control ownership by job_id and independent run_id
Resource locks
Lock owner and conflict messages use job_key
Lock owner is job_id; messages resolve current display labels
Runtime recovery
Recovery state stores Type ID/location
Store job_id and run snapshots so recovery remains stable after edits
Log files and live log
Filename and lookup use mutable job key/type
Use stable ID for lookup; keep readable prefix/name in filenames and content
Notifications
Events contain job_key, Type ID, and location
Correlate by job_id; retain descriptive run snapshots
Reminder state
Deduplication keys contain job_key
Use job_id to prevent duplicate or lost reminders after edits
Overdue detection
Joins schedules, jobs, and latest status by key
Join all three by job_id
Source manifests
Proposed manifests would otherwise be job-keyed
Name and associate manifests by job_id
Import/export
Collision handling rewrites job_key; selected jobs use keys
Define preserve, merge, duplicate, and collision rules for IDs
Configuration backup/restore
Job files and dependent stores use mutable filenames/keys
Include and validate all ID-based references as one consistent configuration set
Support bundle
Diagnostics expose mutable keys from several stores
Include job_id and bounded readable descriptors; add referential-integrity diagnostics
System health
Checks metadata/repository assignments by key
Detect duplicate/missing IDs and dangling ID references across active stores
Job deletion
Deletes artifacts by Type ID and location patterns
Include all new ID-based stores/indexes without changing confirmation semantics
API logging/security context
Logs job_key
Log job_id and bounded readable descriptors; UUID is not secret
Dashboard and live-state rules
The configured job inventory is authoritative.
For every configured job_id, the dashboard combines:
current job metadata,
latest completed status for that job_id,
active runtime state for that job_id,
schedule and overdue state,
restore-test proof,
cached repository information.
Historical status records that no longer map to a configured job never create an additional dashboard card or affect current job counters.
If a migrated job has no new ID-based status yet, its latest unambiguously mapped legacy status may be shown with an indicator such as Last run before identity migration. After the first new run, the normal ID-based status takes precedence.
Reports, history, and historical labels
Reports and History keep continuity when a job is edited:
filtering and grouping use job_id,
the current job name is used for navigation,
each historical row may display the name, archive prefix, location, and repository captured for that run,
changing name or archive prefix does not split one logical job into multiple reports,
changing the current repository does not change report identity,
deleted jobs may remain available as historical jobs identified by former job_id,
ambiguous legacy records are shown separately as unassigned legacy history and are never attached by guesswork.
A historical repository_key is a run snapshot, not an active repository assignment.
Browse and Restore
Browse & Restore selects a job by job_id and resolves:
the one current repository assignment,
the ordered current and historical archive prefixes stored in archive_prefixes,
the current job display name.
The UUID never becomes part of the Borg archive prefix. Existing human-readable archives remain valid.
Repository assignment history is explicitly out of scope. After a repository change, the job uses only the new repository for Browse & Restore. Existing archives remain unchanged in the previous repository. Issue #452 provides the required warning and confirmation.
Restore tests
Restore-test policy and results belong to job_id.
Required behavior:
job name and archive-prefix changes do not reset proof status,
<job_key>.test becomes <job_id>.test,
scheduled and manual tests submit job_id,
overdue reminders and verification maps join by job_id,
result payload retains archive, repository, prefix, location, and display-name snapshots,
deleted jobs can retain historical restore-test results without appearing in the active plan.
Import/export identity rules
Import as a new job: always generate a new job_id, even if the exported source contains one.
Merge/update an explicitly selected existing job: retain the target installation's job_id.
Full configuration restore onto the same installation: preserve IDs when referential integrity is valid.
Importing a complete configuration onto another installation: preserve IDs only when there is no collision; otherwise require an explicit remap plan.
Duplicate job action: generate a new ID.
Never silently merge jobs because name, archive prefix, repository, or legacy key happens to match.
Export bundles include an ID reference map and validate that all selected dependent objects are present.
Migration design
Proposed migration ID:
immutable_job_id_v1
The migration runs before the scheduler, background writers, widget refresh, or writable APIs become available.
Detect phase
Inventory and validate:
all canonical job metadata,
legacy metadata filenames, job_key, backup_type, location, and existing archive_prefixes,
repository used_by / source_job_keys,
schedules,
active and archived .status files,
weekly snapshots,
restore-test files and policies,
restore active runs and restore history,
notification/reminder state,
runtime recovery state,
widget cache,
source manifests if present,
job-keyed logs or control metadata that must survive restart.
Detection returns required: true when any active job lacks a valid UUID, canonical metadata still uses a mutable identity, or a persistent active reference still uses a mutable key.
Planning phase
Build the complete legacy-key-to-UUID plan before modifying data.
Reuse an already journaled UUID if an interrupted attempt is resumed.
Match current jobs by exact canonical legacy metadata key first.
Determine the current explicit archive prefix from the ordered existing archive_prefixes when present; otherwise derive the legacy value once from backup_type by appending -backup.
Preserve and normalize every valid existing prefix without inventing a second prefix-history model.
Use explicit legacy aliases only when they produce one unambiguous match.
Never infer identity from display name alone.
Mark ambiguous or orphaned historical records as unassigned legacy data.
Abort before writes if active configuration references are ambiguous or inconsistent.
Snapshot phase
Create a migration snapshot containing every affected persistent object, including:
job metadata directory,
repository and storage inventories as applicable,
schedules,
status and archived-status records,
weekly snapshots,
restore tests,
restore runs and restore-history index/details,
notification state,
runtime recovery state,
widget cache and persistent source manifests.
Write the migration plan and checksums into the snapshot. Do not include secrets beyond files already required by affected configuration, and never log secret values.
Apply phase
Assign and persist one UUID per active job.
Normalize archive_prefixes so the current complete prefix is first and prior prefixes remain de-duplicated.
Remove Type ID, location, and mutable job_key as active canonical identity fields after dependent readers are converted. Preserve only bounded legacy aliases required by the migration contract.
Rename canonical job metadata files to <job_id>.json.
Convert repository current-assignment references to job IDs.
Convert schedules to job IDs and regenerate cron entries.
Add job_id to unambiguously mapped status records while retaining historical descriptors.
Convert weekly snapshot keys to IDs and merge same-job/same-period entries deterministically.
Rename and update restore-test result files.
Add IDs to restore active runs and restore history records.
Convert reminder/deduplication state without sending duplicate notifications.
Convert runtime recovery records that must survive restart.
Rebuild the widget cache from migrated canonical data.
Invalidate all in-memory job, repository, status, and widget caches.
Verify all active references before marking the migration applied.
Old historical filenames may remain unchanged if their payload has a valid job_id; readers must not depend on the filename. New files use ID-based lookup with readable diagnostic names.
Verification phase
Verify:
every active job has one unique valid UUID,
no UUID is shared by two jobs,
every active job has a non-empty ordered archive_prefixes list,
the first prefix is the exact prefix used for new archives,
no new archive path appends a hidden suffix to that prefix,
every active schedule references an existing job ID,
every current repository job reference resolves,
every restore-test policy/result resolves or is classified as legacy/unassigned,
all mapped statuses resolve to exactly one job ID,
current dashboard counts equal the configured active-job set,
Browse & Restore resolves the same current repository and all stored archive prefixes,
notification reminder state cannot generate duplicate reminders,
no writable service starts when verification fails.
Failure and retry behavior
Follow the existing startup migration contract and audit format.
Record pending, applied, skipped, failed, blocked, or not_applicable.
The operation is idempotent.
Re-running after success performs no changes.
Re-running after interruption uses the persisted migration plan and UUID mapping.
On the first failure, later migrations remain blocked and normal writes/scheduling stay disabled.
Do not automatically roll back the installed plugin version.
Keep the migration snapshot for manual recovery and expose details under System Health and Migration.
Compatibility boundary
During a defined transition period, API request parsing may accept a legacy job_key and resolve it through the migrated alias map.
This is boundary compatibility only:
all internal joins and newly persisted active data use job_id,
UI requests use job_id,
legacy-key use is logged as deprecated without exposing secrets,
ambiguous aliases fail explicitly,
new jobs do not create new job_key identities,
no permanent dual-write identity model remains.
Advantages
Editing a job no longer creates an apparent second job.
The wizard exposes the value that actually controls archive naming.
Users see the complete archive prefix and exact archive pattern before saving.
Dashboard and widget counts remain based on configured jobs.
Reports and History remain continuous across name, prefix, repository, and storage changes.
Restore tests and reminders remain attached to the logical job.
Repository links, schedules, runtime state, cancellation, and locks become stable.
Existing archive_prefixes continuity is preserved without repository history.
Future source manifests and other job-specific features gain a safe owner key.
Deleting one job cannot accidentally match another job's artifacts by filename pattern.
Referential-integrity checks become possible and meaningful.
Disadvantages and risks
This is a broad, high-risk migration touching nearly every job-dependent subsystem.
UUIDs are less readable in files and diagnostics, so UI and logs must resolve friendly labels.
Removing Type ID requires every remaining behavior to use explicit metadata instead of inferred semantics.
Old or already orphaned historical records may not be assignable automatically.
Import/export collision behavior becomes more complex.
External scripts or bookmarked internal API calls using job_key need a bounded compatibility window.
Status, report, and restore readers must support legacy artifacts during transition.
The implementation must be split into carefully reviewable stages while preserving one atomic migration contract.
Required test coverage
Identity and job lifecycle
UUID generation, validation, uniqueness, and immutability.
Create, edit, rename, archive-prefix change, repository change, and storage/location change.
Duplicate and delete behavior.
Job discovery from ID-named metadata.
No Type ID or derived mutable identity for new jobs.
Archive prefix and wizard
New jobs store the complete user-visible prefix exactly.
No hidden -backup suffix is appended.
Prefix preview matches the archive name produced by the runner.
Editing a prefix keeps the new prefix first and preserves prior prefixes.
Existing archive_prefixes migrate without loss or duplication.
Invalid and duplicate active references block migration.
Ambiguous historical records remain unassigned.
Snapshot completeness and audit output.
No later migration or writable service after failure.
Status, dashboard, and widgets
One card per configured job.
Edits do not create a second card.
Legacy status continuity before the first ID-based run.
Active runtime state overrides the last completed state for display.
Orphaned history does not affect active counters.
Widget cache and Homepage widget use the same identity rules.
Reports and History
Continuous time series across name, prefix, repository, and storage changes.
Historical labels remain accurate.
Deleted-job history remains accessible.
Unassigned legacy history is visible but not merged.
Weekly snapshots migrate without duplicate periods.
Browse, Restore, and Restore Tests
Archive filtering uses the complete ordered prefix list in the current repository.
Restore requests and persisted history use job_id.
Name and prefix edits keep restore history and restore proof.
Restore-test files, schedules, policies, reminders, and results migrate correctly.
Repository changes use only the current repository and never move archives.
Repositories, schedules, and runtime
Current repository references reconcile by ID.
Cron invokes jobs by ID.
Overdue detection joins by ID.
Runtime state, cancellation, locks, and recovery remain stable during edits.
Notifications and reminders do not duplicate after migration.
Import/export and diagnostics
New import, merge, full restore, duplicate, and collision cases.
Support bundle contains identity diagnostics without secrets.
System Health reports duplicate IDs, dangling references, and unresolved legacy data.
Acceptance criteria
Every active job has exactly one immutable UUID.
Metadata files are named <job_id>.json and discovered by ID.
Changing job name, archive prefix, repository, or storage does not change job_id.
All active job-dependent areas use job_id internally.
New and migrated jobs do not use Type ID, backup_type, location, or job_key as active identity.
The wizard replaces Type ID with the complete editable archive prefix.
The exact prefix shown in the wizard is used for new archive names without hidden additions.
archive_prefixes[0] is current and previous valid prefixes remain available in the current repository.
Dashboard and widgets display only configured jobs and exactly one entry per job.
Reports, History, Browse & Restore, restore history, and restore tests retain continuity across job edits within the defined current-repository boundary.
Pull request: one draft PR from the integration branch to main
Phase work: separate sub-issues and clearly separated commits on the shared branch
Merge boundary: no phase is merged or released independently
Stable release: a separate release PR only after the complete test candidate is explicitly approved
Parallel hotfixes continue on independent branches from main. After each hotfix is merged, current origin/main is merged into the #447 integration branch without rebasing or force-pushing. The final test candidate is created only after the last such synchronization.
The ordering is intentional. Later phases depend on the contracts and converted stores from earlier phases. Completion of a sub-issue means its scoped code and automated tests are complete on the integration branch; it does not mean that the intermediate application state is installable or releasable.
Test-channel gate
Phases 1-8 do not require or justify a test-channel package merely because their individual code is complete. Until the real migration and every required reader/writer are integrated, an installed intermediate version may not start normally or may have no safe complete workflow to test.
The first test-channel candidate is produced only in #479 after all of the following are true:
the migration detects, plans, snapshots, applies and verifies representative legacy states;
invalid or ambiguous active states block before unsafe changes;
a successful migration releases startup, scheduler and writable APIs;
the migrated application can edit and schedule a job, complete a backup, preserve retention scope, browse archives, restore data and complete a restore test;
restart and retry are idempotent and reuse the same UUID mapping;
the complete automated migration matrix and full source preflight pass;
documentation, backup warning, failure guidance and release notes are complete.
If there is no safe end-to-end behavior for a user to verify, no test package is published. Development continues with unit, fixture and isolated integration tests instead.
Related dependency
Issue #470 remains blocked until #447 is fully completed because managed per-job exclusion files require the immutable job-owned storage contract.
Summary
Jobs currently have no immutable identity. Their effective identity is derived from mutable and overloaded values, primarily:
The wizard exposes
Type IDeven though it is not a real job type. The same user input is used to derive the job key, archive prefix, filenames, cache paths, log correlation, status grouping, and joins across dependent stores. The system also appends-backupwithout making the complete stored archive prefix an explicit user-owned value.Changing Type ID or location therefore looks like deleting one job and creating another to parts of the application. This causes stale or duplicated dashboard entries and can disconnect historical status, reports, restore tests, schedules, notifications, widgets, repository assignments, and restore history from the logical job the user edited.
Introduce a generated immutable
job_id, remove Type ID from the new user-facing and canonical job model, and make the complete archive prefix explicit. Usejob_idas the only canonical foreign key across every job-dependent subsystem.This is an application-wide architecture migration, not a dashboard-only fix.
Current problem
Today different areas infer job identity or behavior from one or more mutable values:
job_key,backup_type/ Type ID,location,-backup,used_by/source_job_keys,For example, changing
paperless_storageboxtodocuments_storageboxremoves the old metadata key but keeps historicalpaperless_storageboxstatus files. The dashboard currently starts with all status keys and only adds missing configured jobs, so both identities may be displayed even though only one configured job exists.The UI model is also unclear:
A user entering
paperlessdoes not directly own or see every value derived from it.Goals
job_idas the only canonical foreign key between job-dependent stores and runtime services.archive_prefixesbehavior: the current prefix is first and previous prefixes remain available for restore and retention scoping.Non-goals
The user-safety warning and confirmation for changing a job repository are tracked separately in #452.
Canonical job model
Example target metadata:
{ "schema_version": 4, "job_id": "6cd32490-99e2-45df-bafb-81b942decd82", "name": "Paperless Backup", "repository_key": "repo_paperless", "archive_prefixes": [ "documents-backup", "paperless-backup" ], "legacy_job_keys": [ "paperless_storagebox" ] }Identity and metadata rules:
job_idis generated once with UUIDv4 and is immutable.<job_id>.json.job_key,backup_type, Type ID, or location as identity or canonical active metadata.nameis the editable display label.repository_keyis the one current repository assignment.archive_prefixesis an ordered, de-duplicated list.archive_prefixes[0]is the complete current prefix used for new archives.archive_prefixesentries are previous complete prefixes used to find and manage existing archives in the current repository.-backupor another hidden suffix.archive_prefixes.legacy_job_keysexists only for migration, legacy-data resolution, and a bounded API compatibility period. It is never a new active foreign key.job_id.job_id.Wizard model
The primary identity and naming fields become:
The wizard must show an exact preview such as:
Required behavior:
Type IDfor new or migrated jobs.archive_prefixes.Run status model
New status records include immutable identity and descriptive run snapshots:
{ "job_id": "6cd32490-99e2-45df-bafb-81b942decd82", "job_name": "Paperless Backup", "archive_prefix": "paperless-backup", "repository_key": "repo_paperless", "location": "storagebox", "archive_name": "paperless-backup-2026-09-02_03-00-00", "status": "success" }Snapshot fields do not need an
_at_runsuffix because a run record is inherently historical. Legacy records may retainbackup_typeandjob_key, but new records and readers must not use them as canonical identity.Status and log filenames remain readable for administrators. Use the archive prefix or a bounded display slug captured for the run plus a short UUID suffix, for example:
The complete UUID remains in the JSON payload, runtime state, and structured log context. The short suffix is only a human-friendly correlation aid and is not used as identity. Readers use payload or persisted run metadata as the source of truth and never infer canonical identity from a readable filename.
Repository assignment boundary
A job has exactly one current
repository_key.When the repository assignment changes:
job_idremains unchanged.repository_historyis added to the job.The dedicated warning and explicit confirmation are implemented separately under #452.
Dependency inventory and required target behavior
job_idjob_key,backup_type, and locationjob_id; expose name, archive prefixes, and current repository separatelyjob_keyjob_id; return readable name and prefix separately.statusper mutable key and supplements configured jobsjob_id; overlay latest run, runtime state, schedule, restore proof, and repository detailsjob_id; one widget row/count per configured jobjob_keyjob_idinternally and return readable descriptors for displayjob_id; keep name, archive prefix, repository, and location as run snapshotsjob_id; preserve historical descriptor metadata where usefuljob_id; support unassigned legacy recordsjob_keyjob_id; name/prefix changes remain one continuous reportjob_id; intentionally display current job labels and historical run snapshotsjob_keyto resolve repository and archive-prefix filtersjob_id; resolve the current repository and orderedarchive_prefixesarchive_prefixes[0]; keep readable historical prefixes for the current repositoryjob_keyjob_idplus run-time job, repository, prefix, and archive snapshotsjob_keyjob_id; keep historical display descriptors<job_key>.test, policies, and plan rows use key<job_id>.test; result payload, policy, scheduling, reminders, and proof map usejob_idschedules.jsonis keyed byjob_key; cron postsjob_keyjob_id; cron postsjob_id; readable descriptors remain diagnostic onlyused_byandsource_job_keyscontain mutable keysjob_ids/source_job_ids; derive display labels from current job inventoryjob_keyjob_keyjob_id; include readable values only in logs and snapshotsjob_idand independentrun_idjob_keyjob_id; messages resolve current display labelsjob_idand run snapshots so recovery remains stable after editsjob_key, Type ID, and locationjob_id; retain descriptive run snapshotsjob_keyjob_idto prevent duplicate or lost reminders after editsjob_idjob_idjob_key; selected jobs use keysjob_idand bounded readable descriptors; add referential-integrity diagnosticsjob_id; require explicit artifact deletion confirmationjob_keyjob_idand bounded readable descriptors; UUID is not secretDashboard and live-state rules
The configured job inventory is authoritative.
For every configured
job_id, the dashboard combines:job_id,job_id,Historical status records that no longer map to a configured job never create an additional dashboard card or affect current job counters.
If a migrated job has no new ID-based status yet, its latest unambiguously mapped legacy status may be shown with an indicator such as
Last run before identity migration. After the first new run, the normal ID-based status takes precedence.Reports, history, and historical labels
Reports and History keep continuity when a job is edited:
job_id,job_id,A historical
repository_keyis a run snapshot, not an active repository assignment.Browse and Restore
Browse & Restore selects a job by
job_idand resolves:archive_prefixes,The UUID never becomes part of the Borg archive prefix. Existing human-readable archives remain valid.
Repository assignment history is explicitly out of scope. After a repository change, the job uses only the new repository for Browse & Restore. Existing archives remain unchanged in the previous repository. Issue #452 provides the required warning and confirmation.
Restore tests
Restore-test policy and results belong to
job_id.Required behavior:
<job_key>.testbecomes<job_id>.test,job_id,job_id,Import/export identity rules
job_id, even if the exported source contains one.job_id.Migration design
Proposed migration ID:
The migration runs before the scheduler, background writers, widget refresh, or writable APIs become available.
Detect phase
Inventory and validate:
job_key,backup_type, location, and existingarchive_prefixes,used_by/source_job_keys,.statusfiles,Detection returns
required: truewhen any active job lacks a valid UUID, canonical metadata still uses a mutable identity, or a persistent active reference still uses a mutable key.Planning phase
archive_prefixeswhen present; otherwise derive the legacy value once frombackup_typeby appending-backup.Snapshot phase
Create a migration snapshot containing every affected persistent object, including:
Write the migration plan and checksums into the snapshot. Do not include secrets beyond files already required by affected configuration, and never log secret values.
Apply phase
archive_prefixesso the current complete prefix is first and prior prefixes remain de-duplicated.job_keyas active canonical identity fields after dependent readers are converted. Preserve only bounded legacy aliases required by the migration contract.<job_id>.json.job_idto unambiguously mapped status records while retaining historical descriptors.Old historical filenames may remain unchanged if their payload has a valid
job_id; readers must not depend on the filename. New files use ID-based lookup with readable diagnostic names.Verification phase
Verify:
archive_prefixeslist,Failure and retry behavior
pending,applied,skipped,failed,blocked, ornot_applicable.Compatibility boundary
During a defined transition period, API request parsing may accept a legacy
job_keyand resolve it through the migrated alias map.This is boundary compatibility only:
job_id,job_id,job_keyidentities,Advantages
archive_prefixescontinuity is preserved without repository history.Disadvantages and risks
job_keyneed a bounded compatibility window.Required test coverage
Identity and job lifecycle
Archive prefix and wizard
-backupsuffix is appended.archive_prefixesmigrate without loss or duplication.Migration
archive_prefixes.Status, dashboard, and widgets
Reports and History
Browse, Restore, and Restore Tests
job_id.Repositories, schedules, and runtime
Import/export and diagnostics
Acceptance criteria
<job_id>.jsonand discovered by ID.job_id.job_idinternally.backup_type, location, orjob_keyas active identity.archive_prefixes[0]is current and previous valid prefixes remain available in the current repository.Phased delivery plan
Issue #447 is delivered through one long-lived integration branch and one final implementation pull request:
codex/issue-447-immutable-job-idsmainParallel hotfixes continue on independent branches from
main. After each hotfix is merged, currentorigin/mainis merged into the #447 integration branch without rebasing or force-pushing. The final test candidate is created only after the last such synchronization.Implementation phases
job_id.immutable_job_id_v1and qualify the end-to-end test candidate.The ordering is intentional. Later phases depend on the contracts and converted stores from earlier phases. Completion of a sub-issue means its scoped code and automated tests are complete on the integration branch; it does not mean that the intermediate application state is installable or releasable.
Test-channel gate
Phases 1-8 do not require or justify a test-channel package merely because their individual code is complete. Until the real migration and every required reader/writer are integrated, an installed intermediate version may not start normally or may have no safe complete workflow to test.
The first test-channel candidate is produced only in #479 after all of the following are true:
If there is no safe end-to-end behavior for a user to verify, no test package is published. Development continues with unit, fixture and isolated integration tests instead.
Related dependency
Issue #470 remains blocked until #447 is fully completed because managed per-job exclusion files require the immutable job-owned storage contract.