diff --git a/docs/features/cmdb-import.md b/docs/features/cmdb-import.md index b54fad71e..31df3bc27 100644 --- a/docs/features/cmdb-import.md +++ b/docs/features/cmdb-import.md @@ -281,7 +281,7 @@ APPID is on neither CMDB sheet: |---|---| | on the sheet `Gearchiveerde Applicaties` | are **archived**: OpenRegister's archive state, with the reason `cmdb-import: on sheet "Gearchiveerde Applicaties"`. Reported as *archived*. | | on no sheet at all | are **deleted**: moved to OpenRegister's trash, where they stay restorable for 30 days. Reported as *deleted*. | -| on no sheet, but the archive sheet cannot be relied on: the workbook has no sheet `Gearchiveerde Applicaties`, the sheet has no `APPID` column, or an `APPID` in it has no cached value (a formula whose result the file does not store), or an `APPID` on one of the two CMDB sheets has no cached value (the import cannot tell which application that row stands for) | are **archived**, never deleted. Each case has its own warning in the result: *sheet not found*, *no column APPID*, or *an APPID without a cached value in row …* (for more rows: *in N rows (…)*, naming the first ten; for a CMDB sheet the warning names that sheet). Such a workbook therefore never moves anything to the trash. | +| on no sheet, but the archive sheet cannot be relied on: the workbook has no sheet `Gearchiveerde Applicaties`, the sheet has no `APPID` column, or an `APPID` in it has no cached value (a formula whose result the file does not store), or an `APPID` on one of the two CMDB sheets has no cached value (the import cannot tell which application that row stands for), or the workbook has only one of the two CMDB sheets (the applications of the other may be among those missing) | are **archived**, never deleted. Each case has its own warning in the result: *sheet not found* (for the archive sheet or the missing CMDB sheet), *no column APPID*, or *an APPID without a cached value in row …* (for more rows: *in N rows (…)*, naming the first ten; for a CMDB sheet the warning names that sheet). Such a workbook therefore never moves anything to the trash. | An archived or deleted application disappears from the stackiq lists, from OpenCatalogi and from Portaliq's **Software we use**. In stackiq, the quick @@ -397,7 +397,7 @@ page. ## Limits -Five limits are read from `lib/Settings/cmdb-import/topdesk-profile.json` on +Six limits are read from `lib/Settings/cmdb-import/topdesk-profile.json` on every import: | Setting | Default | What it limits | diff --git a/l10n/en.js b/l10n/en.js index 108dd98c4..f398bcc42 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -1047,7 +1047,6 @@ OC.L10N.register( "Applications missing from the export": "Applications missing from the export", "Archive or delete them": "Archive or delete them", "Keep them as they are": "Keep them as they are", - "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.": "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.", "An application of this municipality that is on no CMDB sheet any more is archived. It disappears from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. Choose Keep to leave such applications as they are.": "An application of this municipality that is on no CMDB sheet any more is archived. It disappears from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. Choose Keep to leave such applications as they are.", "Applications missing from the export cannot be archived.": "Applications missing from the export cannot be archived.", "OpenRegister is missing or too old to archive objects. Choose \"Keep them as they are\" and import again, or update OpenRegister. Nothing was imported.": "OpenRegister is missing or too old to archive objects. Choose \"Keep them as they are\" and import again, or update OpenRegister. Nothing was imported.", @@ -1212,7 +1211,11 @@ OC.L10N.register( "Loading the name": "Loading the name", "The name could not be loaded. Reload the page to try again.": "The name could not be loaded. Reload the page to try again.", "This contact person is not linked to a Nextcloud contact.": "This contact person is not linked to a Nextcloud contact.", - "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "The name is kept in Nextcloud Contacts, in an address book you cannot read." + "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "The name is kept in Nextcloud Contacts, in an address book you cannot read.", + "Archive them": "Archive them", + "Applications no longer in the export are archived. The (i) button above explains when.": "Applications no longer in the export are archived. The (i) button above explains when.", + "Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.": "Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.", + "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are.": "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index 95a57c106..3ca802f32 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -1046,7 +1046,6 @@ "Applications missing from the export": "Applications missing from the export", "Archive or delete them": "Archive or delete them", "Keep them as they are": "Keep them as they are", - "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.": "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.", "An application of this municipality that is on no CMDB sheet any more is archived. It disappears from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. Choose Keep to leave such applications as they are.": "An application of this municipality that is on no CMDB sheet any more is archived. It disappears from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. Choose Keep to leave such applications as they are.", "Applications missing from the export cannot be archived.": "Applications missing from the export cannot be archived.", "OpenRegister is missing or too old to archive objects. Choose \"Keep them as they are\" and import again, or update OpenRegister. Nothing was imported.": "OpenRegister is missing or too old to archive objects. Choose \"Keep them as they are\" and import again, or update OpenRegister. Nothing was imported.", @@ -1211,6 +1210,10 @@ "Loading the name": "Loading the name", "The name could not be loaded. Reload the page to try again.": "The name could not be loaded. Reload the page to try again.", "This contact person is not linked to a Nextcloud contact.": "This contact person is not linked to a Nextcloud contact.", - "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "The name is kept in Nextcloud Contacts, in an address book you cannot read." + "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "The name is kept in Nextcloud Contacts, in an address book you cannot read.", + "Archive them": "Archive them", + "Applications no longer in the export are archived. The (i) button above explains when.": "Applications no longer in the export are archived. The (i) button above explains when.", + "Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.": "Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.", + "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are.": "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are." } } diff --git a/l10n/nl.js b/l10n/nl.js index 7ed0533e2..6044f4669 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1115,7 +1115,6 @@ OC.L10N.register( "Applications missing from the export": "Applicaties die in de export ontbreken", "Archive or delete them": "Archiveren of verwijderen", "Keep them as they are": "Laat ze zoals ze zijn", - "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.": "Een applicatie van deze gemeente die op geen enkel CMDB-tabblad meer staat, wordt gearchiveerd als het tabblad \"{archive}\" haar noemt, en naar de prullenbak verplaatst als geen enkel tabblad dat doet. Beide verdwijnen uit de lijsten, uit OpenCatalogi en uit Portaliq; een applicatie die in een latere export terugkomt, wordt teruggezet. Ontbreekt dat tabblad, heeft het geen kolom APPID of heeft een APPID geen opgeslagen waarde, dan worden applicaties alleen gearchiveerd en zegt het resultaat dat. Kies Laat ze zoals ze zijn om ze ongemoeid te laten.", "An application of this municipality that is on no CMDB sheet any more is archived. It disappears from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. Choose Keep to leave such applications as they are.": "Een applicatie van deze gemeente die op geen enkel CMDB-tabblad meer staat, wordt gearchiveerd. Ze verdwijnt uit de lijsten, uit OpenCatalogi en uit Portaliq; een applicatie die in een latere export terugkomt, wordt teruggezet. Kies Laat ze zoals ze zijn om zulke applicaties ongemoeid te laten.", "Applications missing from the export cannot be archived.": "Applicaties die in de export ontbreken, kunnen niet worden gearchiveerd.", "OpenRegister is missing or too old to archive objects. Choose \"Keep them as they are\" and import again, or update OpenRegister. Nothing was imported.": "OpenRegister ontbreekt of is te oud om objecten te archiveren. Kies \"Laat ze zoals ze zijn\" en importeer opnieuw, of werk OpenRegister bij. Er is niets geïmporteerd.", @@ -1280,7 +1279,11 @@ OC.L10N.register( "Loading the name": "De naam wordt geladen", "The name could not be loaded. Reload the page to try again.": "De naam kon niet worden geladen. Laad de pagina opnieuw om het nog eens te proberen.", "This contact person is not linked to a Nextcloud contact.": "Deze contactpersoon is niet gekoppeld aan een Nextcloud-contact.", - "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "De naam staat in Nextcloud Contacten, in een adresboek dat je niet kunt lezen." + "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "De naam staat in Nextcloud Contacten, in een adresboek dat je niet kunt lezen.", + "Archive them": "Archiveren", + "Applications no longer in the export are archived. The (i) button above explains when.": "Applicaties die niet meer in de export staan, worden gearchiveerd. De (i)-knop bovenaan legt uit wanneer.", + "Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.": "Een nieuwere export opnieuw importeren werkt dezelfde applicaties bij, herkend aan het APPID per gemeente. Applicaties die er niet meer in staan, worden gearchiveerd, tenzij je kiest ze te laten zoals ze zijn.", + "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are.": "Een applicatie van deze gemeente die op geen enkel CMDB-tabblad meer staat, wordt gearchiveerd als het tabblad \"{archive}\" haar noemt, en naar de prullenbak verplaatst als geen enkel tabblad dat doet. Beide verdwijnen uit de lijsten, uit OpenCatalogi en uit Portaliq; een applicatie die in een latere export terugkomt, wordt teruggezet. Ontbreekt dat tabblad, heeft het geen kolom APPID of heeft een APPID geen opgeslagen waarde, of mist de werkmap een van de CMDB-tabbladen, dan worden applicaties alleen gearchiveerd en zegt het resultaat dat. Kies Laat ze zoals ze zijn om ze ongemoeid te laten." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index 20e6df066..d1ecd4eec 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1114,7 +1114,6 @@ "Applications missing from the export": "Applicaties die in de export ontbreken", "Archive or delete them": "Archiveren of verwijderen", "Keep them as they are": "Laat ze zoals ze zijn", - "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.": "Een applicatie van deze gemeente die op geen enkel CMDB-tabblad meer staat, wordt gearchiveerd als het tabblad \"{archive}\" haar noemt, en naar de prullenbak verplaatst als geen enkel tabblad dat doet. Beide verdwijnen uit de lijsten, uit OpenCatalogi en uit Portaliq; een applicatie die in een latere export terugkomt, wordt teruggezet. Ontbreekt dat tabblad, heeft het geen kolom APPID of heeft een APPID geen opgeslagen waarde, dan worden applicaties alleen gearchiveerd en zegt het resultaat dat. Kies Laat ze zoals ze zijn om ze ongemoeid te laten.", "An application of this municipality that is on no CMDB sheet any more is archived. It disappears from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. Choose Keep to leave such applications as they are.": "Een applicatie van deze gemeente die op geen enkel CMDB-tabblad meer staat, wordt gearchiveerd. Ze verdwijnt uit de lijsten, uit OpenCatalogi en uit Portaliq; een applicatie die in een latere export terugkomt, wordt teruggezet. Kies Laat ze zoals ze zijn om zulke applicaties ongemoeid te laten.", "Applications missing from the export cannot be archived.": "Applicaties die in de export ontbreken, kunnen niet worden gearchiveerd.", "OpenRegister is missing or too old to archive objects. Choose \"Keep them as they are\" and import again, or update OpenRegister. Nothing was imported.": "OpenRegister ontbreekt of is te oud om objecten te archiveren. Kies \"Laat ze zoals ze zijn\" en importeer opnieuw, of werk OpenRegister bij. Er is niets geïmporteerd.", @@ -1279,6 +1278,10 @@ "Loading the name": "De naam wordt geladen", "The name could not be loaded. Reload the page to try again.": "De naam kon niet worden geladen. Laad de pagina opnieuw om het nog eens te proberen.", "This contact person is not linked to a Nextcloud contact.": "Deze contactpersoon is niet gekoppeld aan een Nextcloud-contact.", - "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "De naam staat in Nextcloud Contacten, in een adresboek dat je niet kunt lezen." + "The name is kept in Nextcloud Contacts, in an address book you cannot read.": "De naam staat in Nextcloud Contacten, in een adresboek dat je niet kunt lezen.", + "Archive them": "Archiveren", + "Applications no longer in the export are archived. The (i) button above explains when.": "Applicaties die niet meer in de export staan, worden gearchiveerd. De (i)-knop bovenaan legt uit wanneer.", + "Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.": "Een nieuwere export opnieuw importeren werkt dezelfde applicaties bij, herkend aan het APPID per gemeente. Applicaties die er niet meer in staan, worden gearchiveerd, tenzij je kiest ze te laten zoals ze zijn.", + "An application of this municipality that is on no CMDB sheet any more is archived when the sheet \"{archive}\" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are.": "Een applicatie van deze gemeente die op geen enkel CMDB-tabblad meer staat, wordt gearchiveerd als het tabblad \"{archive}\" haar noemt, en naar de prullenbak verplaatst als geen enkel tabblad dat doet. Beide verdwijnen uit de lijsten, uit OpenCatalogi en uit Portaliq; een applicatie die in een latere export terugkomt, wordt teruggezet. Ontbreekt dat tabblad, heeft het geen kolom APPID of heeft een APPID geen opgeslagen waarde, of mist de werkmap een van de CMDB-tabbladen, dan worden applicaties alleen gearchiveerd en zegt het resultaat dat. Kies Laat ze zoals ze zijn om ze ongemoeid te laten." } } diff --git a/lib/Service/Cmdb/CmdbWorkbookReader.php b/lib/Service/Cmdb/CmdbWorkbookReader.php index 4b80393b8..674179925 100644 --- a/lib/Service/Cmdb/CmdbWorkbookReader.php +++ b/lib/Service/Cmdb/CmdbWorkbookReader.php @@ -179,7 +179,8 @@ public function isAvailable(): bool { * ({sheet, present, keyColumnMissing, appIds, uncachedRows}) and `uncachedKeyRows` * (source sheet => numbers of the rows dropped as empty because their key is a * formula without a cached value; a row with other content is in `rows`, its - * `uncached` lists the key). + * `uncached` lists the key) and `absentSheets` (the profile's source sheets the + * workbook does not have, in profile order). * * @throws CmdbImportException WORKBOOK_TOO_LARGE, READER_UNAVAILABLE, NOT_XLSX, NO_SOURCE_SHEET, * MISSING_COLUMN or TOO_MANY_ROWS. @@ -331,7 +332,7 @@ private function readBounded(string $path, CmdbImportProfile $profile, object $s scanner: $scanner ); - ['present' => $present, 'available' => $available] = $this->sourceSheets(path: $path, profile: $profile); + ['present' => $present, 'absent' => $absent, 'available' => $available] = $this->sourceSheets(path: $path, profile: $profile); $archiveSheet = $profile->archiveSheetName(); $loaded = $present; @@ -406,16 +407,19 @@ private function readBounded(string $path, CmdbImportProfile $profile, object $s 'date1904' => $date1904, 'archive' => $archive, 'uncachedKeyRows' => $uncachedKeyRows, + 'absentSheets' => $absent, ]; }//end readBounded() /** - * The profile's source sheets that the workbook has, and every sheet it has. + * The profile's source sheets that the workbook has, the ones it lacks, and every sheet it has. + * + * A workbook with only some of the source sheets is read; the import must not take it as the whole CMDB. * * @param string $path The xlsx file. * @param CmdbImportProfile $profile The import profile. * - * @return array{present: array, available: array} + * @return array{present: array, absent: array, available: array} * * @throws CmdbImportException NOT_XLSX when the sheet list cannot be read, NO_SOURCE_SHEET when no source sheet is there. */ @@ -441,7 +445,7 @@ private function sourceSheets(string $path, CmdbImportProfile $profile): array { ); } - return ['present' => $present, 'available' => $available]; + return ['present' => $present, 'absent' => array_values(array_diff($expected, $available)), 'available' => $available]; }//end sourceSheets() /** diff --git a/lib/Service/CmdbExportImportService.php b/lib/Service/CmdbExportImportService.php index a96f5f2ca..d9d037cfb 100644 --- a/lib/Service/CmdbExportImportService.php +++ b/lib/Service/CmdbExportImportService.php @@ -299,6 +299,16 @@ class CmdbExportImportService { */ private array $unreadableKeyRows = []; + /** + * The profile's source sheets this run's workbook does not have. + * + * Applications an earlier import took from such a sheet are missing from + * `winningSheets`, so a run with one is not authoritative for a deletion. + * + * @var array + */ + private array $absentSheets = []; + /** * OpenRegister's archive handler and object mapper, once resolved. * @@ -553,6 +563,7 @@ private function runImport(string $path, array $options, string $startedAt): arr $report->addImportWarnings(warnings: $this->translateImportWarnings(warnings: $workbook['importWarnings'])); $archive = $this->archiveSheet(workbook: $workbook); $this->unreadableKeyRows = $this->unreadableKeyRows(rows: $rows, droppedRows: (array)($workbook['uncachedKeyRows'] ?? [])); + $this->absentSheets = array_values(array_map('strval', (array)($workbook['absentSheets'] ?? []))); $this->addRunWarnings(report: $report, municipality: $municipality, archive: $archive, missingRecords: $missingRecords); $this->progressTracker->startOperation( @@ -632,8 +643,8 @@ private function runImport(string $path, array $options, string $startedAt): arr * Add the run's own warnings. * * A mapping that fell back to the packs shipped with stackiq, an archive sheet that is missing, - * has no key column or has unreadable keys, a CMDB sheet with unreadable keys, and a created - * municipality. + * has no key column or has unreadable keys, a CMDB sheet that is missing or has unreadable keys, + * and a created municipality. * * @param CmdbImportReport $report The report. * @param array{uuid: string, name: string, created: bool} $municipality The consumer, as resolveMunicipality() found it. @@ -663,6 +674,11 @@ private function addRunWarnings(CmdbImportReport $report, array $municipality, a } if ($missingRecords === 'archive') { + foreach ($this->absentSheets as $sheet) { + $absent = ['sheet' => $sheet, 'keyColumnMissing' => false, 'uncachedRows' => []]; + $report->addImportWarnings(warnings: [['sheet' => $sheet, 'message' => $this->archiveSheetWarning(archive: $absent)]]); + } + foreach ($this->unreadableKeyRows as $sheet => $rowNumbers) { $cmdbSheet = ['sheet' => $sheet, 'keyColumnMissing' => false, 'uncachedRows' => $rowNumbers]; $report->addImportWarnings(warnings: [['sheet' => $sheet, 'message' => $this->archiveSheetWarning(archive: $cmdbSheet)]]); @@ -2656,8 +2672,8 @@ private function reconcileApplication(array $application, array $archive, string * * Listed on the archive sheet: archived, with that sheet as the reason. * Otherwise soft-deleted, but only when the workbook is authoritative for - * a deletion: it has the archive sheet and no sheet has a row whose key - * could not be read. In every other case it is archived. + * a deletion: it has the archive sheet and every source sheet, and no sheet + * has a row whose key could not be read. In every other case it is archived. * * @param string $key The application key. * @param ArchiveSheet $archive The archive sheet. @@ -2678,9 +2694,9 @@ private function reconcileTarget(string $key, array $archive, string $operationI } $target = CmdbImportReport::ARCHIVED; - if ($archive['present'] === true && $archive['uncachedRows'] === [] && $this->unreadableKeyRows === []) { - // A sheet with an unreadable key (the archive sheet or a CMDB sheet) may list this application, so the - // workbook is not authoritative for a deletion. + if ($archive['present'] === true && $archive['uncachedRows'] === [] && $this->unreadableKeyRows === [] && $this->absentSheets === []) { + // A sheet with an unreadable key (the archive sheet or a CMDB sheet), or a source sheet the workbook + // lacks, may list this application, so the workbook is not authoritative for a deletion. $target = CmdbImportReport::DELETED; } @@ -3023,6 +3039,8 @@ private function resetRun(): void { $this->suppliers = null; $this->seenKeys = []; $this->winningSheets = []; + $this->unreadableKeyRows = []; + $this->absentSheets = []; $this->contactUids = []; $this->contactPersons = []; $this->archiveServices = null; diff --git a/openspec/changes/cmdb-import-archive-reconciliation/design.md b/openspec/changes/cmdb-import-archive-reconciliation/design.md index 5329cc153..dfb88b9a0 100644 --- a/openspec/changes/cmdb-import-archive-reconciliation/design.md +++ b/openspec/changes/cmdb-import-archive-reconciliation/design.md @@ -94,7 +94,7 @@ Per application, with `processed` = every APPID match key on the CMDB sheets (wh | on the archive sheet | deleted | restore, then archive | `archived` | | nowhere, sheet present | working or archived | soft-delete usage, then module | `deleted` | | nowhere, sheet present | deleted | nothing | — | -| nowhere, sheet absent or incomplete (no key column, `uncachedRows` not empty, or a CMDB-sheet row with an unreadable APPID) | working | archive usage, then module | `archived` | +| nowhere, sheet absent or incomplete (no key column, `uncachedRows` not empty, a CMDB-sheet row with an unreadable APPID, or a CMDB sheet absent from the workbook: `absentSheets` not empty) | working | archive usage, then module | `archived` | | nowhere, sheet absent or incomplete | archived or deleted | nothing | — | The table decides on one entry per key. When a municipality has several usages and modules under the same import key (D4 says earlier imports created exactly such duplicates, a module deleted by hand next to the one a later import created), the scope keeps the entry the import would match: the best module state wins (working, then archived, then deleted), the best usage state breaks a tie, the first listed breaks the rest. The other usages of the key follow the application, so a stray usage stays visible nowhere: a working or archived usage of a stale duplicate, a second usage of the same module, and a working or archived duplicate module are archived or soft-deleted as the key asks, once, in the same error boundary and the same report row (usages first, then modules). What is in the trash is left as it is: a stale trashed duplicate is not restored into the archive when the key is on the archive sheet, and is not touched when it is on no sheet. @@ -196,7 +196,7 @@ No seed objects change. The fragment's three seed modules stay as they are; the ## Risks / Trade-offs - [The default archives on the first re-import after the upgrade] → The section shows the choice with help text; the report lists every archived and deleted application; nothing is hard-deleted. -- [A workbook saved without the archive sheet, or whose archive sheet has no APPID column or an APPID without a cached value, or a CMDB sheet with an APPID without a cached value] → archive only, never delete, plus a warning (D3). +- [A workbook saved without the archive sheet, or whose archive sheet has no APPID column or an APPID without a cached value, or a CMDB sheet with an APPID without a cached value, or a workbook without one of the CMDB sheets] → archive only, never delete, plus a warning (D3). - [A failure part way through an application] → usages are written before modules, so a module is never changed under a usage that still shows; but a failing extra usage leaves the own usage changed, that extra visible and the module unchanged, and the row only says `failed`. The next import retries (D5). - [Reviving before writing adds calls per returning row] → only for rows whose match is archived or deleted; a row whose module is working costs the same searches as before, a new row one more search (the archive lens). - [The trash listing scans every magic table of the instance] → read once per import, not per row (D9). diff --git a/openspec/changes/cmdb-import-archive-reconciliation/proposal.md b/openspec/changes/cmdb-import-archive-reconciliation/proposal.md index d9685e710..0531293d2 100644 --- a/openspec/changes/cmdb-import-archive-reconciliation/proposal.md +++ b/openspec/changes/cmdb-import-archive-reconciliation/proposal.md @@ -34,7 +34,7 @@ None. ### In Scope - The sheet "Gearchiveerde Applicaties" (columns Applicatiecomponent, APPID, Applicatie Code, Applicatie Naam, Roepnaam, Applicatie Status, Bron, Datum Interface, Datum wijziging) is read for its APPID column only, when it is present. -- After every row of a completed import: the municipality's usages, with their modules, are compared with the APPIDs on the CMDB sheets and on the archive sheet. Missing and on the archive sheet: module and usage archived. Missing everywhere: module and usage soft-deleted. Without the archive sheet, or when it has no APPID column or an APPID the reader cannot see (a formula without a cached value, on the archive sheet or on a CMDB sheet): archived only, with a warning in the report. +- After every row of a completed import: the municipality's usages, with their modules, are compared with the APPIDs on the CMDB sheets and on the archive sheet. Missing and on the archive sheet: module and usage archived. Missing everywhere: module and usage soft-deleted. Without the archive sheet, or when it has no APPID column or an APPID the reader cannot see (a formula without a cached value, on the archive sheet or on a CMDB sheet), or when the workbook lacks one of the CMDB sheets: archived only, with a warning in the report. - A returning APPID: an archived module or usage is unarchived, a soft-deleted one is restored from the trash, and the row is then updated as before. - `missingRecords`: `keep` leaves everything as it is (the behaviour so far), `archive` reconciles and is the default; `remove` stays reserved and is refused. - The settings section offers the choice with help text; the report shows the new outcomes in its summary and rows. @@ -78,7 +78,7 @@ None. `ArchiveHandler` and `MagicMapper` come from OpenRegister, which stackiq a **Severity:** Medium — **Mitigation:** only the municipality's own usages and the modules they point at are in scope, never a supplier's or another municipality's module; nothing is hard-deleted; the choice is visible in the section with help text, and the report lists every archived and deleted application. A cancelled or failed import never reconciles. ### Risk 2: A workbook without a usable archive sheet soft-deletes everything that moved there -**Severity:** Medium — **Mitigation:** without the sheet, without its APPID column, or with an APPID the reader cannot see on it or on a CMDB sheet (a formula without a cached value), missing applications are archived, never soft-deleted, and the report warns, naming the sheet (and the rows). +**Severity:** Medium — **Mitigation:** without the sheet, without its APPID column, or with an APPID the reader cannot see on it or on a CMDB sheet (a formula without a cached value), or with a CMDB sheet absent from the workbook, missing applications are archived, never soft-deleted, and the report warns, naming the sheet (and the rows). ### Risk 3: The archive services are OpenRegister internals **Severity:** Low — **Mitigation:** resolved through the container behind a guard; when missing, an import with `missingRecords=archive` is refused with 503 `ARCHIVE_UNAVAILABLE` before the file is read, and `keep` still works. diff --git a/openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md b/openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md index a3b5cd2e8..7e941afb2 100644 --- a/openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md +++ b/openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md @@ -16,7 +16,7 @@ OpenRegister: `OCA\OpenRegister\Contract\ObjectServiceInterface` (`searchObjects ### Requirement: An application missing from the CMDB sheets SHALL be archived when the archive sheet lists it and soft-deleted when no sheet does (REQ-CMDB-015) -With `missingRecords` `archive`, after every row of an import that was not cancelled and did not fail, the import SHALL reconcile, under the register lock it already holds, every `usage` whose `consumer` is the imported municipality together with the `module` it points at, including archived and soft-deleted ones, and only those whose module carries this municipality's import key (`topdesk::`). It SHALL read the APPID column of the sheet "Gearchiveerde Applicaties" when the workbook has it, under the same size and row bounds as the CMDB sheets and without reading any other column of it. For an application whose APPID is on neither CMDB sheet: when the archive sheet lists the APPID, the import SHALL archive the usage and then the module through OpenRegister's archive state with a reason that names the import, restoring a soft-deleted one from the trash first; when no sheet lists it, the import SHALL soft-delete the usage and then the module through OpenRegister's trash. When the workbook has no archive sheet, or the sheet has no APPID column, or a row of the sheet holds an APPID formula without a cached value (read as empty, as REQ-CMDB-002 has it for the CMDB sheets, and listed by the reader in `archive.uncachedRows`), the import SHALL archive and SHALL NOT soft-delete, and SHALL add a warning to the report; for an uncached row the warning SHALL name the sheet and the row numbers. The same holds when a row of a CMDB sheet holds an APPID formula without a cached value (the row is skipped as empty, so its application may look absent): the import SHALL archive and SHALL NOT soft-delete, and the warning SHALL name that CMDB sheet and the rows. When a municipality has more than one module or usage under the same import key (a module deleted by hand next to the one a later import created), the import SHALL reconcile as the application the one it would match, a working one before an archived one and an archived one before one in the trash, and SHALL bring every other usage of that key, and every other module of it that is not in the trash, to the same state, so no stray usage stays visible; it SHALL leave what is in the trash as it is, so a stale duplicate is never restored into the archive. An application that is already in the state the export asks for SHALL be left alone and not reported. The import SHALL NOT touch contact persons, organisations, modules that carry another municipality's import key or no import key, or usages of another organisation. Each application SHALL be reconciled in its own error boundary: a failure SHALL be a `failed` report row naming the step and the exception class, and the other applications SHALL still be reconciled. The reconciliation SHALL report its progress as the phase `reconciling` of the import's operation. A cancel SHALL also be honoured after the last row and before each application of the reconciliation: the import SHALL stop there, SHALL keep what it archived or deleted so far, and SHALL report the run as cancelled. The report SHALL count `archived` and `deleted` in its summary and SHALL list each as a row with the APPID, the application name and both uuids, never person data. When OpenRegister's archive handler or object mapper is not available, an import with `missingRecords` `archive` SHALL be refused with 503 `ARCHIVE_UNAVAILABLE` before the file is read. +With `missingRecords` `archive`, after every row of an import that was not cancelled and did not fail, the import SHALL reconcile, under the register lock it already holds, every `usage` whose `consumer` is the imported municipality together with the `module` it points at, including archived and soft-deleted ones, and only those whose module carries this municipality's import key (`topdesk::`). It SHALL read the APPID column of the sheet "Gearchiveerde Applicaties" when the workbook has it, under the same size and row bounds as the CMDB sheets and without reading any other column of it. For an application whose APPID is on neither CMDB sheet: when the archive sheet lists the APPID, the import SHALL archive the usage and then the module through OpenRegister's archive state with a reason that names the import, restoring a soft-deleted one from the trash first; when no sheet lists it, the import SHALL soft-delete the usage and then the module through OpenRegister's trash. When the workbook has no archive sheet, or the sheet has no APPID column, or a row of the sheet holds an APPID formula without a cached value (read as empty, as REQ-CMDB-002 has it for the CMDB sheets, and listed by the reader in `archive.uncachedRows`), the import SHALL archive and SHALL NOT soft-delete, and SHALL add a warning to the report; for an uncached row the warning SHALL name the sheet and the row numbers. The same holds when a row of a CMDB sheet holds an APPID formula without a cached value (the row is skipped as empty, so its application may look absent): the import SHALL archive and SHALL NOT soft-delete, and the warning SHALL name that CMDB sheet and the rows. The same holds when the workbook lacks one of the profile's CMDB sheets while it has another (the reader lists the absent ones in `absentSheets`; a workbook with none of them is refused with `NO_SOURCE_SHEET`): the import SHALL archive and SHALL NOT soft-delete, and the warning SHALL name the missing sheet. When a municipality has more than one module or usage under the same import key (a module deleted by hand next to the one a later import created), the import SHALL reconcile as the application the one it would match, a working one before an archived one and an archived one before one in the trash, and SHALL bring every other usage of that key, and every other module of it that is not in the trash, to the same state, so no stray usage stays visible; it SHALL leave what is in the trash as it is, so a stale duplicate is never restored into the archive. An application that is already in the state the export asks for SHALL be left alone and not reported. The import SHALL NOT touch contact persons, organisations, modules that carry another municipality's import key or no import key, or usages of another organisation. Each application SHALL be reconciled in its own error boundary: a failure SHALL be a `failed` report row naming the step and the exception class, and the other applications SHALL still be reconciled. The reconciliation SHALL report its progress as the phase `reconciling` of the import's operation. A cancel SHALL also be honoured after the last row and before each application of the reconciliation: the import SHALL stop there, SHALL keep what it archived or deleted so far, and SHALL report the run as cancelled. The report SHALL count `archived` and `deleted` in its summary and SHALL list each as a row with the APPID, the application name and both uuids, never person data. When OpenRegister's archive handler or object mapper is not available, an import with `missingRecords` `archive` SHALL be refused with 503 `ARCHIVE_UNAVAILABLE` before the file is read. #### Scenario: An application that moved to the archive sheet is archived @e2e tests/e2e/spec-coverage/cmdb-import.spec.ts @@ -61,6 +61,14 @@ With `missingRecords` `archive`, after every row of an import that was not cance - **THEN** the module and the usage of `8` SHALL be archived and SHALL NOT be deleted - **AND** the report SHALL carry a warning naming the CMDB sheet and the row +#### Scenario: A workbook that lacks one of the CMDB sheets never turns an archive into a deletion +@e2e exclude Covered by the service and reader tests; tests/Unit/Service/CmdbExportImportServiceTest.php testAMissingSourceSheetArchivesInsteadOfDeleting asserts the archive state, no delete, and the warning naming the missing sheet; tests/Unit/Service/Cmdb/CmdbWorkbookReaderTest.php testAWorkbookWithSomeSourceSheetsNamesTheAbsentOnes asserts the reader lists the absent sheet. + +- **GIVEN** the module with APPID `8` and its usage were imported from the sheet "Beheerde Applicaties CMDB" +- **WHEN** a workbook with "Onbeh Applicaties CMDB" and a readable archive sheet, but without "Beheerde Applicaties CMDB", is imported with `archive` +- **THEN** the module and the usage of `8` SHALL be archived and SHALL NOT be deleted +- **AND** the report SHALL carry a warning naming "Beheerde Applicaties CMDB" + #### Scenario: Keeping missing records, a cancelled import and another municipality leave everything as it is @e2e exclude Covered by the service test; tests/Unit/Service/CmdbExportImportServiceTest.php testRecordsMissingFromTheExportStayWhenKept, testACancelledImportDoesNotReconcile and testAnotherMunicipalitysUsagesAreNeverReconciled assert no archive, no delete and no reconciliation row. diff --git a/openspec/changes/cmdb-import-archive-reconciliation/tasks.md b/openspec/changes/cmdb-import-archive-reconciliation/tasks.md index cf59a6d3d..be258b8b3 100644 --- a/openspec/changes/cmdb-import-archive-reconciliation/tasks.md +++ b/openspec/changes/cmdb-import-archive-reconciliation/tasks.md @@ -45,7 +45,7 @@ Status in this PR (the reconciliation slice, on top of the reader slice): Tasks - **files**: `lib/Service/CmdbExportImportService.php`, `lib/Service/ProgressTracker.php`, `lib/Exception/CmdbImportException.php`, `tests/Unit/Service/CmdbExportImportServiceTest.php` - **acceptance_criteria**: - GIVEN APPIDs 1 and 7 imported WHEN an export with row 1 and 7 on the archive sheet is imported THEN module and usage of 7 are archived with a reason, the report row is `archived`, and the owner contact person is untouched - - GIVEN APPID 7 on no sheet THEN module and usage are soft-deleted and the row is `deleted`; GIVEN the archive sheet absent, without an APPID column, or with an APPID that has no cached value, or a CMDB-sheet row whose APPID has no cached value THEN they are archived instead and the report warns + - GIVEN APPID 7 on no sheet THEN module and usage are soft-deleted and the row is `deleted`; GIVEN the archive sheet absent, without an APPID column, or with an APPID that has no cached value, or a CMDB-sheet row whose APPID has no cached value, or a workbook without one of the CMDB sheets, THEN they are archived instead and the report warns - GIVEN APPID 7 soft-deleted and listed on the archive sheet THEN module and usage are restored and archived; GIVEN it already archived or, when on no sheet, already deleted THEN nothing is written - GIVEN a working pair and a trashed pair under the same import key and the key on the archive sheet THEN the working pair is archived, nothing is restored and the trashed pair stays deleted; GIVEN a further working usage of the key (of the trashed module, or a second one of the working module) THEN it is archived or deleted with the application - GIVEN `missingRecords` `keep`, a cancelled import, or a usage of another municipality THEN nothing is archived or deleted diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 362945e88..60f0880bc 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -936,7 +936,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModuleversie" + "board": [ + "SkModuleversie" + ] } }, { @@ -972,7 +974,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkSuiteWizard" + "board": [ + "SkSuiteWizard" + ] } }, { @@ -1008,7 +1012,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -1039,6 +1045,10 @@ "bluedolphin": "https://help.bluedolphin.io/en/articles/11967603-manage-object-questionnaires: 'A questionnaire is a group of fields that add more details to objects' configured by admins, which can hold a sector field; no sector list is shipped (read 2026-09-26). Reached on: Admin > Object questionnaires.", "sap-leanix": "https://help.sap.com/docs/leanix/ea/tags-and-custom-fields: tags and tag groups for 'quick, high-level classification', filterable and usable as reporting views; no predefined government sector list is documented (read 2026-09-26). Reached on: Fact sheet > Tags.", "glpi": "source read at 11.0.9: no government sector concept, grep -i 'sector' over src/*.php and locales/glpi.pot hits only menu sectorization (src/Html.php:1019); the nearest holder is the single-valued Appliance type dropdown install/mysql/glpi-empty.sql:8941 appliancetypes_id, or an admin custom dropdown (install/mysql/glpi-empty.sql:10113 glpi_dropdowns_dropdowndefinitions) used as a field of a custom asset. Multi-valued tagging needs the separate tag plugin (github.com/pluginsGLPI/tag, not read). Reached on: Management > Appliances, Appliance type field." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -1113,7 +1123,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -1148,7 +1160,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -1184,7 +1198,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -1220,7 +1236,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -1293,7 +1311,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkDatakwaliteit" + "board": [ + "SkDatakwaliteit" + ] } }, { @@ -1367,7 +1387,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -1402,7 +1424,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkSuiteWizard" + "board": [ + "SkSuiteWizard" + ] } }, { @@ -1437,7 +1461,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModuleversie" + "board": [ + "SkModuleversie" + ] } }, { @@ -1473,7 +1499,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -1509,7 +1537,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKoppelingen" + "board": [ + "SkKoppelingen" + ] } }, { @@ -1545,7 +1575,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKoppelingen" + "board": [ + "SkKoppelingen" + ] } }, { @@ -1581,7 +1613,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -1617,7 +1651,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKoppelingen" + "board": [ + "SkKoppelingen" + ] } }, { @@ -1652,7 +1688,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -1687,7 +1725,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKoppelingen" + "board": [ + "SkKoppelingen" + ] } }, { @@ -1722,7 +1762,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -1758,7 +1800,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKoppelingen" + "board": [ + "SkKoppelingen" + ] } }, { @@ -1793,7 +1837,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -1829,7 +1875,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -1865,7 +1913,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -1901,7 +1951,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -1937,7 +1989,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -1973,7 +2027,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -2049,7 +2105,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -2084,7 +2142,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -2119,7 +2179,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -2154,7 +2216,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -2189,7 +2253,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -2224,7 +2290,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -2260,7 +2328,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkStandaarden" + "board": [ + "SkStandaarden" + ] } }, { @@ -2296,7 +2366,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkComplianceMatrix" + "board": [ + "SkComplianceMatrix" + ] } }, { @@ -2332,7 +2404,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkComplianceMatrix" + "board": [ + "SkComplianceMatrix" + ] } }, { @@ -2368,7 +2442,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkComplianceMatrix" + "board": [ + "SkComplianceMatrix" + ] } }, { @@ -2404,7 +2480,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -2440,7 +2518,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkStandaarden" + "board": [ + "SkStandaarden" + ] } }, { @@ -2476,7 +2556,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkComplianceMatrix" + "board": [ + "SkComplianceMatrix" + ] } }, { @@ -2546,6 +2628,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for Forum Standaardisatie, no hits (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea for Forum Standaardisatie and comply or explain, no hits (read 2026-09-26)", "glpi": "source read at 11.0.9: grep -rli 'standaard\\|forum standaardisatie\\|comply' over src/ locales/glpi.pot returns nothing relevant; no comply or explain list in core (Setup menu src/Html.php:1330 onwards)." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -2614,6 +2700,10 @@ "vng-softwarecatalogus": "unknown: no questionnaire to suppliers is described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "sap-leanix": "https://help.sap.com/docs/leanix/ea/reviewing-responses: survey answers are reviewed and saved to the fact sheet; recipients are the users 'assigned to the fact sheet'. Sending to an outside supplier is not documented (read 2026-09-26). Reached on: Surveys.", "glpi": "source read at 11.0.9: native forms (src/Glpi/Form/Form.php:92) are filled by logged in or helpdesk users and only produce tickets, changes or problems (src/Glpi/Form/Destination/FormDestinationTicket.php:47); nothing sends a questionnaire to a supplier or stores answers on an appliance." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -2649,7 +2739,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -2685,7 +2777,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -2720,7 +2814,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisaties" + "board": [ + "SkOrganisaties" + ] } }, { @@ -2751,6 +2847,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; a vendor community is referenced in the docs (https://help.bluedolphin.io/en/articles/11967582-iso27001-management-system-template 'a post on the community'), but finding peers who use the same product is not documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; a vendor community exists (https://community.leanix.net/) but no in product way to find organisations using the same product is documented (read 2026-09-26)", "glpi": "source read at 11.0.9: each GLPI instance holds one organisation's data (entities are internal subdivisions, src/Entity.php), so there is no view of other organisations using the same product; grep -rli 'peer' src/*.php matches only relation and network code such as src/CommonDBConnexity.php, no peer organisation feature." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -2825,7 +2925,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -2861,7 +2963,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -2896,7 +3000,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -2970,7 +3076,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkRoadmap" + "board": [ + "SkRoadmap" + ] } }, { @@ -3006,7 +3114,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkRoadmap" + "board": [ + "SkRoadmap" + ] } }, { @@ -3079,7 +3189,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkRapportages" + "board": [ + "SkRapportages" + ] } }, { @@ -3226,6 +3338,10 @@ "vng-softwarecatalogus": "unknown: no strategic goals are described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "sap-leanix": "https://help.sap.com/docs/leanix/ea/objective-modeling-guidelines: 'Objectives link to business capabilities and initiatives so progress can be tracked over time'. Applications reach objectives through capabilities and initiatives; a direct application to objective relation is not described (read 2026-09-26). Reached on: Objective fact sheet.", "glpi": "source read at 11.0.9: grep -i 'strategic\\|goal\\|objective' over src/Appliance.php and src/Project.php returns nothing; no goal itemtype exists and the Appliance tabs (src/Appliance.php:98 onwards) link items, contracts, documents, tickets and projects only." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3340,7 +3456,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkContract" + "board": [ + "SkContract" + ] } }, { @@ -3492,7 +3610,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkLicenties" + "board": [ + "SkLicenties" + ] } }, { @@ -3562,6 +3682,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; no entitlement against consumption computation is documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; no entitlement against consumption computation is documented (read 2026-09-26)", "glpi": "source read at 11.0.9: entitlement is compared with licence assignments (src/SoftwareLicense.php:158 computeValidityIndicator), while measured installations are counted separately (src/Item_SoftwareVersion.php:39); core has no computed effective licence position report reconciling the two for an audit. Reached on: Assets > Software > Licenses and Installations tabs." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3592,6 +3716,10 @@ "vng-softwarecatalogus": "unknown: contracts, licences and costs are not described anywhere in the public docs (the 2021 user survey asks which other tool municipalities use for contract management); searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier, https://www.softwarecatalogus.nl/gebruikersonderzoek%202021 (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for budget, no hits (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; the contract has a 'Contract Cost Center' string (https://help.sap.com/docs/leanix/ea/contract-extension-to-meta-model) but charging costs against a budget with a period is not documented (read 2026-09-26)" + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3622,6 +3750,10 @@ "vng-softwarecatalogus": "unknown: contracts, licences and costs are not described anywhere in the public docs (the 2021 user survey asks which other tool municipalities use for contract management); searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier, https://www.softwarecatalogus.nl/gebruikersonderzoek%202021 (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for depreciation, no hits (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea for depreciation and amortisation, no hits (read 2026-09-26)" + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3658,7 +3790,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkContract" + "board": [ + "SkContract" + ] } }, { @@ -3731,7 +3865,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKwetsbaarheden" + "board": [ + "SkKwetsbaarheden" + ] } }, { @@ -3804,7 +3940,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkModuleversie" + "board": [ + "SkModuleversie" + ] } }, { @@ -3835,6 +3973,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for CVE, no hits (read 2026-09-26)", "sap-leanix": "https://help.sap.com/docs/leanix/ea/api-updates-sbom-async-processing: 'Switching to asynchronous processing will allow us to add additional post-processing mechanisms in the future, such as vulnerability checks. Though there's no established timeline for these enhancements' (read 2026-09-26)", "glpi": "source read at 11.0.9: grep -rli 'cve' over src/ templates/ finds no feed matching (only src/Glpi/System/Requirement/PhpSupportedVersion.php:74 text); no pluginsGLPI cve repository exists (git ls-remote https://github.com/pluginsGLPI/cve: repository not found)." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3865,6 +4007,10 @@ "vng-softwarecatalogus": "unknown: no vulnerability, SBOM or risk functions are described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; risk can be captured in questionnaires ('Risk Assessment' example in https://help.bluedolphin.io/en/articles/11967710-using-the-odata-feed), but a risk score from vulnerabilities and support status is not documented (read 2026-09-26)", "glpi": "source read at 11.0.9: with no vulnerability data (only hit src/Glpi/System/Requirement/PhpSupportedVersion.php:74) and no support status field on software (install/mysql/glpi-empty.sql:6856 glpi_softwares), no risk score is computed; grep -i 'risk' over src/Appliance.php src/Software.php returns nothing." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3895,6 +4041,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for patch and vulnerability, no hits (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; no per vulnerability patch status is documented, and vulnerability checks are a future item (https://help.sap.com/docs/leanix/ea/api-updates-sbom-async-processing) (read 2026-09-26)", "glpi": "source read at 11.0.9: installed versions are known (src/Item_SoftwareVersion.php:39) but no vulnerability or fixed in version exists to compare against (grep -rli 'cve' src/ hits only src/Glpi/System/Requirement/PhpSupportedVersion.php:74)." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -3929,7 +4079,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkKwetsbaarheden" + "board": [ + "SkKwetsbaarheden" + ] } }, { @@ -3964,7 +4116,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisaties" + "board": [ + "SkOrganisaties" + ] } }, { @@ -4000,7 +4154,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkBeheerToegang" + "board": [ + "SkBeheerToegang" + ] } }, { @@ -4031,6 +4187,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; concept, active and inactive states for organisations are not documented (read 2026-09-26)", "sap-leanix": "unknown: https://help.sap.com/docs/leanix/ea/organization-modeling-guidelines does not describe concept, active and inactive states for organizations; only archiving of fact sheets in general (https://help.sap.com/docs/leanix/ea/archiving-deleting-and-recovering-fact-sheets) (read 2026-09-26)", "glpi": "source read at 11.0.9: a supplier is active or inactive, src/Supplier.php:365 is_active search option (install/mysql/glpi-empty.sql:7087 glpi_suppliers.is_active); there is no concept state. Reached on: Management > Suppliers, Active field." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -4065,7 +4225,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -4101,7 +4263,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -4137,7 +4301,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -4172,7 +4338,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -4207,7 +4375,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -4242,7 +4412,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkBeheerToegang" + "board": [ + "SkBeheerToegang" + ] } }, { @@ -4273,6 +4445,10 @@ "vng-softwarecatalogus": "unknown: login is by username and password (\"Vul uw GEMMA Softwarecatalogus-gebruikersnaam in\"); no identity provider sign-in is described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier, https://www.softwarecatalogus.nl/user/login (read 2026-09-26)", "bluedolphin": "https://help.bluedolphin.io/en/articles/11967579-configure-single-sign-on-sso-with-okta-saml and https://help.bluedolphin.io/en/articles/11967575-configure-single-sign-on-sso-with-office-365-openid: SSO setup guides for Okta SAML, ADFS and Office 365 OpenID (read 2026-09-26). Reached on: Admin > Identity providers.", "sap-leanix": "https://help.sap.com/docs/leanix/ea/managing-users: 'SSO : You manage access through your identity provider (IdP) system. Users sign in through your IdP'; guides for Entra ID, Okta, OneLogin (read 2026-09-26). Reached on: Administration > Single Sign-On." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -4303,6 +4479,10 @@ "vng-softwarecatalogus": "unknown: no directory sync is described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "https://help.bluedolphin.io/en/articles/11967618-automatic-user-provisioning-with-scim-2-0: 'BlueDolphin supports automatic user provisioning ... manage BlueDolphin users and their role assignments', with guides for Entra ID, Okta and SailPoint (read 2026-09-26). Reached on: Admin > SCIM provisioning.", "sap-leanix": "https://help.sap.com/docs/leanix/ea/scim-provisioning: 'SCIM facilitates the transfer of user information from a source system, such as an external identity provider (IdP), to a target system, such as SAP LeanIX'; setup guides for Entra ID and Okta (read 2026-09-26). Reached on: Administration > SCIM provisioning." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -4333,6 +4513,10 @@ "bluedolphin": "https://help.bluedolphin.io/en/articles/11967481-user-profile: users see 'Profile > Personal information'; https://help.bluedolphin.io/en/articles/11967487-forgotten-password: 'To have your password reset, you need to contact your BlueDolphin administrator' and change the temporary password after login (read 2026-09-26). Reached on: Account > Profile.", "sap-leanix": "https://help.sap.com/docs/leanix/ea/user-profile: 'Manage your user profile settings. Change or reset your password if needed' on the My Settings profile page (read 2026-09-26). Reached on: User menu > My Settings.", "glpi": "source read at 11.0.9: front/preference.php renders the user's own form through src/User.php:3105 showMyForm (template pages/admin/user/user.html.twig), whose preference variant shows a 'Change password' button to front/updatepassword.php at templates/pages/admin/user/user.html.twig:277 to :279; the new password form is templates/password_form.html.twig:79." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -4367,7 +4551,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkBeheerToegang" + "board": [ + "SkBeheerToegang" + ] } }, { @@ -4403,7 +4589,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -4435,6 +4623,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; no open data publication is documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; no open data publication is documented (read 2026-09-26)", "glpi": "source read at 11.0.9: no open data publication exists (grep -rli 'open data' src/ returns nothing); anonymisation settings cover ticket actors only (install/mysql/glpi-empty.sql:2824 anonymize_support_agents on entities)." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -4470,7 +4662,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -4506,7 +4700,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -4542,7 +4738,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -4687,7 +4885,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -4758,7 +4958,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -4793,7 +4995,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -4828,7 +5032,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkDashboard" + "board": [ + "SkDashboard" + ] } }, { @@ -4863,7 +5069,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkDashboard" + "board": [ + "SkDashboard" + ] } }, { @@ -4898,7 +5106,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkRapportages" + "board": [ + "SkRapportages" + ] } }, { @@ -4934,7 +5144,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkRapportages" + "board": [ + "SkRapportages" + ] } }, { @@ -5004,6 +5216,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; surveys can be scheduled (https://bluedolphin.io/application-portfolio-management-application-rationalization/ 'Keep data current by scheduling surveys'), but sending reports on a schedule is not documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; scheduled notification digests (https://help.sap.com/docs/leanix/ea/notifications-center) and scheduled snapshot exports by API (https://help.sap.com/docs/leanix/ea/export) exist, but mailing a report to named people on a schedule is not documented (read 2026-09-26)", "glpi": "source read at 11.0.9: saved search alerts, src/SavedSearch_Alert.php:45 with count conditions (src/SavedSearch_Alert.php:53 to :58) and a frequency, run by src/SavedSearch_Alert.php:307 cronSavedSearchesAlerts and sent through the notification system to configured recipients; they notify on a result count rather than sending the report itself. Reached on: Tools > Saved searches > Alerts tab." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5077,7 +5293,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkFunctiesRoadmap" + "board": [ + "SkFunctiesRoadmap" + ] } }, { @@ -5150,7 +5368,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -5181,6 +5401,10 @@ "vng-softwarecatalogus": "unknown: operational IT management functions are not described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; the DataCollector uploads data from databases and files (https://help.bluedolphin.io/en/articles/11967629-import-and-export-options-with-bluedolphin), an installation discovery agent is not documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; LeanIX has no own installation agent, software assets come in through the ServiceNow integration (https://help.sap.com/docs/leanix/ea/obsolescence-risk-management-import-software-assets) (read 2026-09-26)" + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5211,6 +5435,10 @@ "vng-softwarecatalogus": "unknown: operational IT management functions are not described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; network discovery is not documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; network device discovery is not documented (read 2026-09-26)" + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5241,6 +5469,10 @@ "vng-softwarecatalogus": "unknown: operational IT management functions are not described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for SaaS discovery, no hits (read 2026-09-26)", "glpi": "source read at 11.0.9: inventory handlers under src/Glpi/Inventory/Asset/ (Software.php, Process.php, VirtualMachine.php and others) read what the agent sees on devices; grep -i 'saas' over src/ returns nothing, so cloud subscriptions nobody registered are not discovered. The software handler is src/Glpi/Inventory/Asset/Software.php:56." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5275,7 +5507,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkCmdb" + "board": [ + "SkCmdb" + ] } }, { @@ -5382,6 +5616,10 @@ "vng-softwarecatalogus": "unknown: operational IT management functions are not described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; incidents stay in the ITSM tool, logging them against an application in BlueDolphin is not documented (read 2026-09-26)", "sap-leanix": "https://help.sap.com/docs/leanix/ea/portals: an Application Portal 'accessible to everyone to order software, issue support tickets' through configurable links to the helpdesk; incidents themselves stay in the ITSM tool (read 2026-09-26). Reached on: Portals > links to helpdesk." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5412,6 +5650,10 @@ "vng-softwarecatalogus": "unknown: operational IT management functions are not described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; a change approval workflow on an application is not documented (read 2026-09-26)", "sap-leanix": "unknown: docs searched at https://help.sap.com/docs/leanix/ea; architecture decisions have a review process (https://help.sap.com/docs/leanix/ea/architecture-decisions) but a change approval workflow on an application is not documented (read 2026-09-26)" + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5442,6 +5684,10 @@ "vng-softwarecatalogus": "unknown: operational IT management functions are not described; searched https://www.softwarecatalogus.nl/node/16564, https://www.softwarecatalogus.nl/node/13683, https://www.softwarecatalogus.nl/node/19703, https://www.softwarecatalogus.nl/Gebruikershandleiding_leverancier (read 2026-09-26)", "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/ for SLA and service level, no hits (read 2026-09-26)", "sap-leanix": "https://help.sap.com/docs/leanix/ea/contract-extension-to-meta-model: 'Service Level Agreement (SLA) Single Select Corporate-Level SLA, Customer-Level SLA, Service-Level SLA' and an SLA description on the contract. No tracking of targets against results (read 2026-09-26). Reached on: Contract fact sheet > Governance and Regulations." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5472,6 +5718,10 @@ "bluedolphin": "unknown: docs searched at https://help.bluedolphin.io/en/; the process portal publishes processes (https://help.bluedolphin.io/en/articles/11967500-getting-started-with-process-publication-portal), but requesting software is not documented (read 2026-09-26)", "sap-leanix": "https://help.sap.com/docs/leanix/ea/portals: 'create an Application Portal that is accessible to everyone to order software ... Action button: Perform an action, in this case \"Request new Application\"' (read 2026-09-26). Reached on: Portals > Application Portal.", "glpi": "source read at 11.0.9: the 11.0 service catalog src/Glpi/Form/ServiceCatalog/ServiceCatalog.php:45 lists native forms for self service users (menu src/Html.php:1283), and a form question can offer software items to pick, src/Glpi/Form/QuestionType/QuestionTypeItem.php:180 Software::class, creating a ticket from the answer. Reached on: Self service > Service catalog. Driven on the lab at 11.0.9 (2026-09-26): /ServiceCatalog shows the service catalog with \"Report an issue\" and \"Request a service\"." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5502,6 +5752,10 @@ "bluedolphin": "unknown: https://help.bluedolphin.io/en/articles/11967477-minimum-system-requirements lists desktop browsers (Chrome, Firefox, Edge) and says Safari 'is not supported'; a native mobile app is not documented (read 2026-09-26)", "sap-leanix": "https://updates.leanix.net/announcements/explore-portals-on-mobile-devices (2025-09-18): 'Since the mobile app was decommissioned in June 2023, we planned to make portals mobile responsive'; only portals are mobile friendly (read 2026-09-26)", "glpi": "source read at 11.0.9: grep -rli 'android\\|ios app\\|mobile app' over src/ templates/ returns nothing; the repository ships only the responsive web interface (templates/) and APIs (src/Glpi/Api/HL/Controller/CoreController.php:322 docs), no native mobile client." + }, + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." } }, { @@ -5537,7 +5791,9 @@ }, "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -5573,7 +5829,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -5609,7 +5867,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -5645,7 +5905,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -5681,7 +5943,9 @@ "topdesk": "partial", "screen": { "canvas": "design-system", - "board": "SkBeheerToegang" + "board": [ + "SkBeheerToegang" + ] } }, { @@ -5717,7 +5981,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -5749,7 +6015,11 @@ "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "no", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "life-value-assessment", @@ -5819,7 +6089,11 @@ "sap-leanix": "partial", "bluedolphin": "unknown", "glpi": "no", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "comp-processing-register-generate", @@ -5850,7 +6124,11 @@ "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "no", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "ctr-collective-agreements", @@ -5881,7 +6159,11 @@ "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "no", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "share-compliance-documents", @@ -5990,7 +6272,11 @@ "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "partial", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "org-act-as-user", @@ -6021,7 +6307,11 @@ "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "yes", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "comp-common-ground-fit", @@ -6052,7 +6342,11 @@ "sap-leanix": "unknown", "bluedolphin": "unknown", "glpi": "no", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "land-version-carry-connections", @@ -6122,7 +6416,11 @@ "vng-softwarecatalogus": "unknown", "sap-leanix": "partial", "bluedolphin": "unknown", - "glpi": "partial" + "glpi": "partial", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "land-dependent-fields", @@ -6157,7 +6455,9 @@ "glpi": "partial", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -6193,7 +6493,9 @@ "glpi": "partial", "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -6229,7 +6531,9 @@ "glpi": "no", "screen": { "canvas": "design-system", - "board": "SkDashboard" + "board": [ + "SkDashboard" + ] } }, { @@ -6261,7 +6565,11 @@ "glpi": "yes", "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", - "bluedolphin": "unknown" + "bluedolphin": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "sec-password-policy", @@ -6292,7 +6600,11 @@ "vng-softwarecatalogus": "unknown", "sap-leanix": "yes", "bluedolphin": "unknown", - "glpi": "yes" + "glpi": "yes", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "ops-change-risk-score", @@ -6323,7 +6635,11 @@ "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", - "glpi": "no" + "glpi": "no", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "conn-auto-populate-dependencies", @@ -6358,7 +6674,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -6390,7 +6708,11 @@ "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", - "topdesk": "partial" + "topdesk": "partial", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "ctr-contract-owner", @@ -6460,7 +6782,11 @@ "vng-softwarecatalogus": "unknown", "sap-leanix": "unknown", "bluedolphin": "unknown", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "org-assigned-only-rights", @@ -6491,7 +6817,11 @@ "vng-softwarecatalogus": "unknown", "sap-leanix": "partial", "bluedolphin": "unknown", - "topdesk": "partial" + "topdesk": "partial", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "ops-own-house-style", @@ -6562,7 +6892,9 @@ "topdesk": "partial", "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -6599,7 +6931,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -6635,7 +6969,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -6671,7 +7007,9 @@ "topdesk": "partial", "screen": { "canvas": "design-system", - "board": "SkDashboard" + "board": [ + "SkDashboard" + ] } }, { @@ -6708,7 +7046,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -6740,7 +7080,11 @@ "vng-softwarecatalogus": "unknown", "bluedolphin": "unknown", "glpi": "no", - "topdesk": "unknown" + "topdesk": "unknown", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "arch-diagram-version-compare", @@ -6775,7 +7119,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -6811,7 +7157,9 @@ "topdesk": "partial", "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -6843,7 +7191,11 @@ "vng-softwarecatalogus": "partial", "sap-leanix": "unknown", "glpi": "no", - "topdesk": "partial" + "topdesk": "partial", + "screen": { + "board": null, + "reason": "Besloten dit niet te bouwen (decided-no), dus er is geen scherm." + } }, { "id": "land-move-between-workspaces", @@ -6878,7 +7230,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -6940,7 +7294,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -6976,7 +7332,9 @@ "topdesk": "unknown", "screen": { "canvas": "design-system", - "board": "SkModelleren" + "board": [ + "SkModelleren" + ] } }, { @@ -7057,7 +7415,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkApplicaties" + "board": [ + "SkApplicaties" + ] } }, { @@ -7096,7 +7456,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkKoppelingen" + "board": [ + "SkKoppelingen" + ] } }, { @@ -7135,7 +7497,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkGebruikDetail" + "board": [ + "SkGebruikDetail" + ] } }, { @@ -7216,7 +7580,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkGebruikDetail" + "board": [ + "SkGebruikDetail" + ] } }, { @@ -7255,7 +7621,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -7294,7 +7662,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -7372,7 +7742,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkComplianceMatrix" + "board": [ + "SkComplianceMatrix" + ] } }, { @@ -7411,7 +7783,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -7450,7 +7824,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkGemmaPlaat" + "board": [ + "SkGemmaPlaat" + ] } }, { @@ -7489,7 +7865,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -7528,7 +7906,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkBeheerToegang" + "board": [ + "SkBeheerToegang" + ] } }, { @@ -7687,7 +8067,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkInstellingenGegevens" + "board": [ + "SkInstellingenGegevens" + ] } }, { @@ -7810,7 +8192,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkContracten" + "board": [ + "SkContracten" + ] } }, { @@ -7893,7 +8277,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkContract" + "board": [ + "SkContract" + ] } }, { @@ -8015,7 +8401,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkPubliek" + "board": [ + "SkPubliek" + ] } }, { @@ -8054,7 +8442,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkOrganisatie" + "board": [ + "SkOrganisatie" + ] } }, { @@ -8214,7 +8604,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkPubliek" + "board": [ + "SkPubliek" + ] } }, { @@ -8366,7 +8758,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkInstellingenUitwisseling" + "board": [ + "SkInstellingenUitwisseling" + ] } }, { @@ -8405,7 +8799,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkApplicatie" + "board": [ + "SkApplicatie" + ] } }, { @@ -8487,7 +8883,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkDashboard" + "board": [ + "SkDashboard" + ] } }, { @@ -8525,7 +8923,9 @@ "source": "pending-moved-2026-10-07", "screen": { "canvas": "design-system", - "board": "SkFunctiesRoadmap" + "board": [ + "SkFunctiesRoadmap" + ] } }, { @@ -8581,6 +8981,11 @@ "SkGebruikDetail" ] }, + "ai-system-inventory": { + "board": [ + "SkApplicatie" + ] + }, "app-id-rename": { "board": null, "reason": "Interne migratie van de app-id, geen scherm." @@ -8671,6 +9076,15 @@ "SkInstellingenGegevens" ] }, + "contract-administration": { + "board": [ + "SkApplicatie", + "SkContract", + "SkContracten", + "SkLicenties", + "SkRapportages" + ] + }, "contract-decision-delegation": { "board": [ "SkContract" @@ -8764,6 +9178,13 @@ "SkLicenties" ] }, + "maintenance-and-supplier-roadmap": { + "board": [ + "SkApplicatie", + "SkDashboard", + "SkModuleversie" + ] + }, "method-decomposition": { "board": null, "reason": "Refactoring van controllers en services, geen scherm." @@ -8789,6 +9210,12 @@ "SkOrganisatie" ] }, + "open-data-publishing": { + "board": [ + "SkApplicaties", + "SkPubliek" + ] + }, "org-archimate-export": { "board": [ "SkInstellingenGegevens" diff --git a/src/manifest.d/usages.json b/src/manifest.d/usages.json index ec21a35fe..928e0b5b9 100644 --- a/src/manifest.d/usages.json +++ b/src/manifest.d/usages.json @@ -24,7 +24,7 @@ { "key": "moduleVersion", "label": "Version", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "labelField": "version" } }, "status", { "key": "businessOwner", "label": "Business owner", "widget": "contactName" }, - "technicalOwner", + { "key": "technicalOwner", "label": "Technical owner", "widget": "contactName" }, "timeClassification", "serviceDeskRecordId" ], diff --git a/src/utils/cmdbImport.js b/src/utils/cmdbImport.js index bafa6d8b4..fe979eee0 100644 --- a/src/utils/cmdbImport.js +++ b/src/utils/cmdbImport.js @@ -1239,12 +1239,48 @@ export function missingApplicationsText(archive = mappingArchiveSheet(null)) { } return t( 'stackiq', - 'An application of this municipality that is on no CMDB sheet any more is archived when the sheet "{archive}" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column, or has an APPID without a cached value, applications are only archived and the result says so. Choose Keep to leave them as they are.', + 'An application of this municipality that is on no CMDB sheet any more is archived when the sheet "{archive}" lists it, and moved to the trash when no sheet does. Both disappear from the lists, from OpenCatalogi and from Portaliq; one that returns in a later export is brought back. When that sheet is missing, has no APPID column or has an APPID without a cached value, or the workbook lacks one of the CMDB sheets, applications are only archived and the result says so. Choose Keep to leave them as they are.', { archive }, AS_TEXT, ) } +/** + * The words of the "Applications missing from the export" choice and of the + * info-panel line on re-imports: they promise the trash only when the profile + * names an archive sheet, because without one the import only archives. + * + * @param {string|null} [archive] The archive sheet, from mappingArchiveSheet(); the shipped name when left out, null when the profile names none + * @return {{archiveOption: string, archiveHelp: string, reimportInfo: string}} The texts + * @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015 + */ +export function missingRecordsTexts(archive = mappingArchiveSheet(null)) { + if (!archive) { + return { + archiveOption: t('stackiq', 'Archive them'), + archiveHelp: t( + 'stackiq', + 'Applications no longer in the export are archived. The (i) button above explains when.', + ), + reimportInfo: t( + 'stackiq', + 'Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived, unless you choose to keep them.', + ), + } + } + return { + archiveOption: t('stackiq', 'Archive or delete them'), + archiveHelp: t( + 'stackiq', + 'Applications no longer in the export are archived or moved to the trash. The (i) button above explains when.', + ), + reimportInfo: t( + 'stackiq', + 'Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived or deleted, unless you choose to keep them.', + ), + } +} + /** * The words for a pack's target: what the pack's rows become. * diff --git a/src/utils/cmdbImport.spec.js b/src/utils/cmdbImport.spec.js index 52f0a32a6..f06217de2 100644 --- a/src/utils/cmdbImport.spec.js +++ b/src/utils/cmdbImport.spec.js @@ -600,6 +600,7 @@ describe('the archive sheet in the help texts', () => { const withArchive = missingApplicationsText('Archief') expect(withArchive).toContain('archived when the sheet "Archief" lists it') expect(withArchive).toContain('moved to the trash') + expect(withArchive).toContain('or the workbook lacks one of the CMDB sheets') const without = missingApplicationsText(null) expect(without).not.toContain('trash') expect(without).not.toContain('sheet "') diff --git a/src/views/settings/sections/CmdbImport.spec.js b/src/views/settings/sections/CmdbImport.spec.js index a7a64a7c7..852cb1627 100644 --- a/src/views/settings/sections/CmdbImport.spec.js +++ b/src/views/settings/sections/CmdbImport.spec.js @@ -381,6 +381,13 @@ describe('CmdbImport sheet names in the help texts', () => { const panel = wrapper.find('.cmdb-import-info').text() expect(panel).not.toContain('Gearchiveerde Applicaties') expect(panel).not.toContain('moved to the trash') + expect(panel).not.toContain('archived or deleted') + const choice = wrapper + .find('[data-testid="cmdb-import-missing-records"]') + .text() + expect(wrapper.vm.missingRecordsWords.archiveOption).toBe('Archive them') + expect(choice).not.toContain('delete') + expect(choice).not.toContain('trash') await wrapper.setData({ error: tooLarge }) expect(wrapper.vm.errorView.hint).toContain( '(it reads only "Alpha", "Beta")', diff --git a/src/views/settings/sections/CmdbImport.vue b/src/views/settings/sections/CmdbImport.vue index b552150fd..b0dd806a0 100644 --- a/src/views/settings/sections/CmdbImport.vue +++ b/src/views/settings/sections/CmdbImport.vue @@ -138,7 +138,7 @@ type="radio" :disabled="importing" data-testid="cmdb-import-missing-records-archive"> - {{ t('stackiq', 'Archive or delete them') }} + {{ missingRecordsWords.archiveOption }}

- {{ - t( - 'stackiq', - 'Applications no longer in the export are archived or moved to the trash. The (i) button above explains when.', - ) - }} + {{ missingRecordsWords.archiveHelp }}

@@ -414,12 +409,7 @@ }}
  • - {{ - t( - 'stackiq', - 'Importing a newer export again updates the same applications, matched on APPID per municipality. Applications missing from it are archived or deleted, unless you choose to keep them.', - ) - }} + {{ missingRecordsWords.reimportInfo }}
  • {{ t('stackiq', 'Update existing records') }}

    @@ -502,6 +492,7 @@ import { mappingSheetNames, MISSING_RECORDS_MODES, missingApplicationsText, + missingRecordsTexts, moduleUrl, municipalityOptions, normaliseError, @@ -652,6 +643,17 @@ export default { return missingApplicationsText(this.archiveSheet) }, + /** + * The words of the missing-records choice and of the re-import line, + * which mention deleting only when the profile names an archive sheet. + * + * @return {{archiveOption: string, archiveHelp: string, reimportInfo: string}} The texts + * @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015 + */ + missingRecordsWords() { + return missingRecordsTexts(this.archiveSheet) + }, + /** * Whether everything the import needs has been chosen. * diff --git a/tests/Unit/Service/Cmdb/CmdbWorkbookReaderTest.php b/tests/Unit/Service/Cmdb/CmdbWorkbookReaderTest.php index fa5754636..39554e1c1 100644 --- a/tests/Unit/Service/Cmdb/CmdbWorkbookReaderTest.php +++ b/tests/Unit/Service/Cmdb/CmdbWorkbookReaderTest.php @@ -995,6 +995,41 @@ public function testTheArchiveSheetIsOptionalAndReadForItsKeyOnly(): void { } }//end testTheArchiveSheetIsOptionalAndReadForItsKeyOnly() + /** + * A workbook with only some of the source sheets is read, and names the ones it lacks; with all of them it names none. + * + * @return void + * + * @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015 + */ + public function testAWorkbookWithSomeSourceSheetsNamesTheAbsentOnes(): void { + $this->requireSpreadsheet(); + $cmdb = [['APPID', 'Applicatie Naam'], [1, 'Een']]; + $archive = [['APPID'], [7]]; + $cases = [ + 'one source sheet missing' => [ + ['Onbeh Applicaties CMDB' => $cmdb, 'Gearchiveerde Applicaties' => $archive], + ['Beheerde Applicaties CMDB'], + ], + 'every source sheet there' => [ + ['Onbeh Applicaties CMDB' => $cmdb, 'Beheerde Applicaties CMDB' => $cmdb, 'Gearchiveerde Applicaties' => $archive], + [], + ], + ]; + + foreach ($cases as $case => [$sheets, $absent]) { + $path = CmdbTestSupport::buildWorkbook(sheets: $sheets); + try { + $result = (new CmdbWorkbookReader())->read(path: $path, profile: $this->profile()); + } finally { + unlink($path); + } + + $this->assertSame($absent, $result['absentSheets'], $case); + $this->assertTrue($result['archive']['present'], $case); + } + }//end testAWorkbookWithSomeSourceSheetsNamesTheAbsentOnes() + /** * An archive sheet with more rows than the profile allows is refused and named, like a source sheet. * diff --git a/tests/Unit/Service/CmdbExportImportServiceTest.php b/tests/Unit/Service/CmdbExportImportServiceTest.php index dffbf6182..c8e6a583d 100644 --- a/tests/Unit/Service/CmdbExportImportServiceTest.php +++ b/tests/Unit/Service/CmdbExportImportServiceTest.php @@ -998,8 +998,8 @@ function (&$value): void { * * @return CmdbWorkbookReader */ - private function rowsReader(array $rows, ?array $archived = [], array $uncachedRows = [], array $uncachedKeyRows = []): CmdbWorkbookReader { - return new class($rows, $archived, $uncachedRows, $uncachedKeyRows) extends CmdbWorkbookReader { + private function rowsReader(array $rows, ?array $archived = [], array $uncachedRows = [], array $uncachedKeyRows = [], array $absentSheets = []): CmdbWorkbookReader { + return new class($rows, $archived, $uncachedRows, $uncachedKeyRows, $absentSheets) extends CmdbWorkbookReader { /** * Constructor. * @@ -1007,12 +1007,14 @@ private function rowsReader(array $rows, ?array $archived = [], array $uncachedR * @param array|null $archived The archive sheet's APPIDs, or null. * @param array $uncachedRows The archive sheet's rows without a cached APPID. * @param array> $uncachedKeyRows The CMDB rows dropped as empty, per sheet. + * @param array $absentSheets The source sheets the workbook lacks. */ public function __construct( private array $rows, private ?array $archived, private array $uncachedRows, private array $uncachedKeyRows = [], + private array $absentSheets = [], ) { } @@ -1026,7 +1028,14 @@ public function __construct( */ public function read(string $path, CmdbImportProfile $profile): array { $archive = ['sheet' => 'Gearchiveerde Applicaties', 'present' => ($this->archived !== null), 'keyColumnMissing' => false, 'appIds' => ($this->archived ?? []), 'uncachedRows' => $this->uncachedRows]; - return ['rows' => $this->rows, 'importWarnings' => [], 'date1904' => false, 'archive' => $archive, 'uncachedKeyRows' => $this->uncachedKeyRows]; + return [ + 'rows' => $this->rows, + 'importWarnings' => [], + 'date1904' => false, + 'archive' => $archive, + 'uncachedKeyRows' => $this->uncachedKeyRows, + 'absentSheets' => $this->absentSheets, + ]; } }; }//end rowsReader() @@ -2176,6 +2185,32 @@ public function testAFullyEmptyCmdbRowWithAnUncachedKeyArchivesInsteadOfDeleting ); }//end testAFullyEmptyCmdbRowWithAnUncachedKeyArchivesInsteadOfDeleting() + /** + * A workbook that lacks one of the source sheets is not the whole CMDB: what is on no sheet is archived, not deleted, + * and the import names the missing sheet. + * + * @return void + * + * @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015 + */ + public function testAMissingSourceSheetArchivesInsteadOfDeleting(): void { + $this->importOneSevenAndEight(); + $eight = $this->moduleOf(appId: '8'); + + $report = $this->service( + reader: $this->rowsReader(rows: [$this->row(appId: '1')], archived: ['7'], absentSheets: ['Beheerde Applicaties CMDB']) + )->import(path: '', options: ['municipalityUuid' => 'muni-1']); + + $this->assertNotContains('delete', array_column($this->transitions, 0), 'nothing is soft-deleted'); + $this->assertSame([true, false], $this->stateOf(schema: self::MODULE, uuid: $eight), 'APPID 8 may be on the missing sheet, so it is archived'); + $this->assertSame([true, false], $this->stateOf(schema: self::USAGE, uuid: $this->usageOf(moduleUuid: $eight))); + $this->assertSame(0, $report['summary']['deleted']); + $this->assertSame( + [['sheet' => 'Beheerde Applicaties CMDB', 'message' => 'Sheet "Beheerde Applicaties CMDB" was not found, so applications missing from the export are archived, not deleted.']], + $report['importWarnings'] + ); + }//end testAMissingSourceSheetArchivesInsteadOfDeleting() + /** * An extra usage of a key that cannot be archived fails the application's row; its module, written after every usage, stays * as it was, and the other applications are still reconciled. diff --git a/tests/vitest/usages.spec.js b/tests/vitest/usages.spec.js index 3bbc33a9f..fe5e876c4 100644 --- a/tests/vitest/usages.spec.js +++ b/tests/vitest/usages.spec.js @@ -99,6 +99,18 @@ describe('the usage pages', () => { expect(statuses).toEqual(usage.properties.status.enum) }) + it('resolves both owner columns to the contact name, not the contact person uuid', () => { + // A contact person carries only `contactsUid`; the name lives in + // Nextcloud Contacts, so a plain column would print the uuid. + const columns = page('Gebruik').config.columns + for (const owner of ['businessOwner', 'technicalOwner']) { + const column = columns.find( + (c) => (typeof c === 'string' ? c : c.key) === owner, + ) + expect(column, owner).toMatchObject({ widget: 'contactName' }) + } + }) + it('shows version, status and owners on the detail page and offers the lifecycle', () => { const detail = page('GebruikDetail') expect(detail.route).toBe('/gebruik/:id')