Skip to content

Release: merge development into beta - #1269

Open
github-actions[bot] wants to merge 155 commits into
betafrom
development
Open

github-actions[bot] wants to merge 155 commits into
betafrom
development

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

Changelist

Snapshot of development at af446eb (16 Oct 2026) against beta at 612cad4: 12 PRs, 3 more bundled inside them. The chore(sync) PR that carries beta back into development is left out. PRs merged to development after this snapshot are not listed yet.

The Rotterdam CMDB import (WOO-586, WOO-587, WOO-588): archive reconciliation, mapping packs run from OpenRegister with a link to their editor, server-side search in the Applications and Services lists, contact person names, and @conduction/nextcloud-vue 2.73.1.

Features (3)

Fixes (3)

Dependencies and security (3)

Maintenance (CI, gates, specs, docs) (3)

github-actions Bot and others added 30 commits October 8, 2026 09:51
…0261008095136

chore(sync): carry beta back into development
…x 47 @SPEC paths

Two findings from Thijn's re-review of the beta promotion (#1026), after
#1261 restored the frozen widget id:

Restoring `softwarecatalog_concept_organisaties_widget` keeps the widget for
users from before the rename, but drops it for everyone who added it while
stable 0.1.148 to 0.2.0 and beta registered it as
`stackiq_concept_organisaties_widget`. The Dashboard app keeps that choice in
`oc_preferences` under its own app id `dashboard`, key `layout`, as a
comma-separated list of widget ids, which IConfig reaches like this app's
own rows. The new post-migration repair step RepointDashboardWidgetId rewrites
the renamed id to the frozen one for every seen user, in place and without
listing the frozen id twice; a layout without the renamed id is left alone.
The widget comment and the app-id-rename spec no longer claim the rows are
out of reach.

The archive pass (1aa2346, 7339b35) moved four changes without updating
their @SPEC tags: wizard-dataset-card-load (4), portal-contribution (8),
publication-field-rules (20) and rename-app-id-to-stackiq (14), 47 tags in
16 files now point at openspec/specs/<capability>/spec.md. The anchors were
checked against the synced specs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… runs

Hydra gate-110: a repair step added without moving <version> in
appinfo/info.xml makes `occ upgrade` answer "No upgrade required." and run
none of them. 0.2.4-unstable.20261001120000 → 0.2.5-unstable.20261008120000,
the same shape as #997 for the EOL repair step.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fix(dashboard): re-point dashboards from the renamed widget id and fix 47 @SPEC paths
chore(parity): normalise features in the capability matrix
…cations list by _ids

The Applications page sent the facet/search match as a plain `id` filter,
which OpenRegister's objects endpoint ignores (verified on 2.1.36: `id[]=`
returns the whole list, `_ids[]=` narrows), so a search or GEMMA facet
selection narrowed nothing while the counts said it did. The narrowing
filter now uses `_ids` (and the no-match sentinel still yields an empty
list); the helper has its own spec.

Reference columns rendered the raw uuid: `provider` on Applications, the
organisation on an application's Usages, the application on an
organisation's usages, connections and compliance claims, and the module on
Module versions and Applications in use. Those columns are now column
objects with the library's `fkResolve` cell widget (uuid → name through the
object store) and their own label — an object-typed property gets no
schema-generated column, so a bare key showed the key as its header.

An organisation's Applications list named no application (only licence and
BBN level) and had no way to the rest beyond "+N more"; it now leads with
the name and links to the Applications page filtered on the organisation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review of #1275 (f1): "In development since" and "In use since" were the
only two of the thirteen new column labels missing from nl.json, so Dutch
users saw them in English. Added to en.json and nl.json and the browser
files rebuilt with `npm run l10n:build`; `npm run test:l10n` passes (every
used key present in en.json).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
chore(parity): every spec under a feature, every capability on a screen
CI on #1275: `quality / Frontend Check (format)` flagged the three new or
changed JS/Vue files (`npx prettier --write` on them; no semantic change,
`npm run format` now clean) and Hydra gate-16 spec-coverage flagged
`facetNarrowingFilter` as a changed method without an @SPEC tag — the tag
now sits on the function docblock (both anchors are requirement headings in
openspec/specs/gemma-faceted-search/spec.md). eslint 0 errors; the helper's
spec still passes (3 tests).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ings

fix(ui): name references in the catalogue tables and narrow the Applications list by _ids
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.15.2 to 8.2.0.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/8.2/CHANGELOG.md)
- [Commits](guzzle/guzzle@7.15.2...8.2.0)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-version: 8.2.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
… archived

Adds the OpenSpec change cmdb-import-archive-reconciliation (REQ-CMDB-015..017,
REQ-CMDB-012 and REQ-CMDB-014 modified). The register fragment opts module
(0.3.9) and usage (1.5.7) into OpenRegister's archive state, the import
profile names the sheet "Gearchiveerde Applicaties" and the accepted
missingRecords modes, and the workbook reader returns the APPIDs of that
sheet, read under the same bounds as the CMDB sheets for its key column
only. The archived-applications fixture covers it.

First of three slices of #1281; nothing reads the archive sheet yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…export

With missingRecords archive, the default, a completed import reconciles the
municipality's usages and the modules its import created: an APPID on the
archive sheet is archived, one on no sheet is soft-deleted, and without the
archive sheet missing applications are archived only, with a report
warning. A returning application is unarchived or restored before it is
updated, never duplicated. Object state is read per lens (working, then
_archived=true) and from the trash, because OpenRegister's list search
hydrates no archive marker and drops soft-deleted rows. The endpoint
accepts missingRecords keep or archive and translates ARCHIVE_UNAVAILABLE;
OpenAPI and Postman describe the new values and outcomes.

Second of three slices of #1281, on top of the archive-sheet slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uick filter

The CMDB import section lets the admin archive or delete applications
missing from the export, or keep them, and shows the archived, unarchived,
deleted and restored outcomes. The Applications and Applications in use
pages get an Archived quick filter. Vitest and the Playwright spec cover
it; the feature docs and the changelog describe it.

Third of three slices of #1281, on top of the reconciliation slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the OpenSpec change cmdb-import-mapping-view (ADDED REQ-CMDB-020,
MODIFIED REQ-CMDB-014) and the admin-only GET
/api/settings/cmdb-import/mapping. It loads the import profile and its five
packs through CmdbImportProfile, the loader the import uses, and answers the
flat {profile, packs}; a broken pack answers 503 MAPPING_UNAVAILABLE with
the loader's reason.

First of two slices of #1280.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt section

A read-only mapping view in the CMDB import section shows which sheet
column feeds which field. Vitest and the Playwright spec cover it; the
feature docs describe it.

Second of two slices of #1280, on top of the endpoint slice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ive them the Search and Columns sidebar

A free-text search on the faceted Applications page narrowed the list by
every id the facets matched, sent as _ids[] in the query string. From
about 170 matches the request line was too long, the server answered
HTTP 414 and the list showed an error on every search after that.

The page now has the Search and Columns sidebar the other index pages
have. Its search is the list's own _search, which OpenRegister pages
server side; the facet counts follow the same term. Only a GEMMA facet
selection still narrows the list by the matched ids. The Services page
uses the same view and gets the same sidebar, as its toolbar search box
is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rt-import spec

The fragment test was renamed to testTheMergedModuleIsVersion039WithTheExternalIds
when the module schema moved to 0.3.9; the live spec still named the 038 test.
The archived change keeps its historical name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…' into feat/woo587-cmdb-import-archive-reconciliation-2-reconcile
trash() set the cached trash to empty before it read the listing, so a
listing that threw failed its row and left every later row of the run
matching against an empty trash: a soft-deleted module that returned was
created a second time. The cache is now set only once the listing was
read; the next row reads it again. The missing-archive-services path
keeps caching the empty trash, since a retry cannot succeed there.

Mutation-checked: testAFailedTrashReadIsNotCachedForTheNextRow fails on
the previous trash() (outcomes and read count) and passes on this one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se list

The business owner column showed the contact person's uuid. A contact
person in OpenRegister carries only a contactsUid; the name is in
Nextcloud Contacts (the CMDB import puts its owners in the importing
admin's "Stackiq CMDB owners" address book).

GET /api/contact-names?ids[]=<uuid> gives the names for a signed-in
user: it reads the contact persons with the caller's own OpenRegister
rights and looks each contactsUid up in the address books Nextcloud
gives the caller. A name is only returned to someone who can already
read both the contact person and the contact. There is no public route
and nothing is stored, so the owner stays unreadable for anonymous
visitors, OpenCatalogi and portaliq.

The new contactName cell widget batches the cells of one page into one
request and keeps the uuid when the caller has no name for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The release build runs npm ci, so it bundles the version in the lock.
2.73.1 carries nextcloud-vue#1390: the page-size selector of an index
page works again, and the Related panel of a detail page opens the
related object and shows names instead of uuids. The floor in
package.json moves with it, so an install can never resolve below the
version that has those fixes.

The lock also drops "dev": true from five optional platform binaries;
that is the newer npm writing the lock, not a dependency change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…export-import change

openspec/changes/cmdb-export-import was archived to
openspec/changes/archive/2026-10-05-cmdb-export-import, so the tags this
change added on addRunWarnings() and the module write path named a file
that no longer exists. The older tags in lib/ share the stale path and are
left for a separate sweep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ile' into feat/woo587-cmdb-import-archive-reconciliation-3-ui
WilcoLouwerse and others added 22 commits October 13, 2026 20:59
… live region that renders empty first

The live region was inserted already holding the loading text and a load
failure while the block was collapsed was silent. The region now renders
empty, the load starts after mount (mounted + nextTick), and the same region
announces loading and a short translatable failure message (en, nl).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lass

The fail-closed fragment check pushed CmdbImportPackRules over phpmd's
class and method complexity limits (schemaFields: cyclomatic 13, NPath
288; class 58), which turned PHP Quality (phpmd) red. Reading the register
and its fragments, and deciding why a property may not be mapped, moves to
CmdbRegisterSchemaFields; the rules class asks it once. Behaviour is
unchanged: --filter Cmdb stays at 347 green, and a control mutation in the
new class turns two of them red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chive-reconciliation-3-ui

feat(cmdb-import): offer the missing-records choice and an Archived quick filter
…chive-reconciliation-2-reconcile

feat(cmdb-import): archive or soft-delete applications that left the export
…pping-view-2-ui

feat(cmdb-import): show where each import pack comes from and link to its editor in OpenRegister
…sidebar

fix(ui): search the Applications and Services lists server side, with the Search and Columns sidebar
feat(contacts): show a contact person's name in Applications in use, on its page and in the organisation's list
…on in their own method

reconcileApplication() reached the phpmd cyclomatic complexity threshold
of 10 once the archive-sheet branch landed. The choice between archiving
and soft-deleting, with its archive reason, moves to reconcileTarget();
the writes and their error boundary stay where they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed-list requirement

The indexPageKey watcher and onQuickFilterChange() keep the Archived-tab
note in step with the active tab; gate 16 needs their @SPEC tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…db-import-archive-reconciliation-1-reader

Brings in #1290 (sidebar search sent as _search) and #1292 (contact
person names).

FacetedCatalogIndexView: development's remount key (listRemount) and
sidebar search are kept; this branch's Archived-tab note, the tab
tracking and the watcher that resets it on a remount are kept on top.

Since #1290 the list sends the search term as _search, outside the
filter, so the Archived tab's empty `_ids` no longer drops it. The note,
the manifest _note, the docs, the task and the tests now say the tab
ignores the GEMMA filters only; the note's string changed key in en/nl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…' into feat/woo588-cmdb-import-mapping-view-1-endpoint

Brings in #1285-#1287 (archive or soft-delete applications that left the
export) and, through it, development with #1290 and #1292.

Both sides are kept, with these choices:
- topdesk-profile.json stays at version 1.0.0, the first version for the
  administrator, with #1285's description (the archive sheet and the
  missingRecords modes).
- The sheet texts keep this branch's list-based functions and name the
  archive sheet there: sheetsReadText() and the "delete the sheets the
  import does not read" hint. The old "{first}"/"{second}" keys are
  replaced in en/nl, not kept next to the new ones; l10n/*.js regenerated.
- The shipped-fallback warning moves into addRunWarnings(), where #1285
  put the run's other warnings, ahead of them as before.
- The section keeps the one-sentence option help and puts the
  missing-applications explanation in the info panel.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…chive-reconciliation-1-reader

feat(cmdb-import): read the archive sheet and let module and usage be archived
CVE-2026-109413 (unbounded recursion in the YAML inline lexer) affects
symfony/yaml below 6.4.45, which made gate 4 (composer-audit) fail. A
dev dependency through consolidation/robo; only the lock file changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is missing

Since openregister#4602 is on OpenRegister's development branch, the
PHPUnit job's OpenRegister ships MigrationPackRefusedException and the
other pack-store classes itself. loadHardenedOpenRegister() declared the
stand-ins regardless, and the run stopped with "Cannot declare class
OCA\OpenRegister\Exception\MigrationPackRefusedException". The fake now
passes the slug to PackSlugExistsException, as the real constructor
takes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ster's constructors

The stand-ins had no constructors and no getErrors(), while the real
classes take array $errors, string $message and string $packSlug. CI
runs the real classes and a local run the stand-ins, so a test could
pass locally and fail in CI. The stand-ins now mirror OpenRegister's
signatures and messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hipped one

The help texts took the source sheets from the mapping endpoint but the
archive sheet from a constant, and the information panel named
"Gearchiveerde Applicaties" literally. mappingOverview() now answers
profile.archiveSheet (null when the profile names none), and the info
text, the too-large hint and the missing-applications paragraph use it,
with the shipped name only until the mapping has loaded. Without an
archive sheet they leave it out, and the paragraph says the import only
archives, as reconcileTarget() does. Tests cover a named sheet, none
and the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cmdb-import-mapping-view and cmdb-import-archive-reconciliation each
modified REQ-CMDB-014 in full, each without what the other adds, so
archiving the second would have dropped the first one's text. Both now
carry one block with the missing-applications choice, the four counts,
the mapping block and the archive sheet from the profile, plus a note
that a change to it goes into both. The payload list, design D4 and the
tasks name the archive sheet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
perfectionist/sort-named-imports wants MISSING_RECORDS_MODES before
missingApplicationsText.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pping-view-1-endpoint

feat(cmdb-import): run the mapping packs stored in OpenRegister, seeded from the shipped files
Comment thread src/manifest.json
{ "id": "org-modules", "type": "object-list", "title": "Applications", "icon": "Package", "content": { "register": "@resolve:voorzieningen_register", "schema": "module", "filter": { "provider": "@objectId" }, "columns": [ { "key": "licentietype", "label": "License type" }, { "key": "bbnLevel", "label": "BBN level" } ], "limit": 25, "rowRoute": "ModuleDetail", "emptyText": "No applications registered for this organisation yet" } },
{ "id": "org-usages", "type": "object-list", "title": "Applications in use", "icon": "OfficeBuilding", "content": { "register": "@resolve:voorzieningen_register", "schema": "usage", "filter": { "consumer": "@objectId" }, "columns": [ { "key": "module", "label": "Application" }, { "key": "moduleVersion", "label": "Version" }, { "key": "status", "label": "Status" } ], "limit": 25, "rowRoute": "GebruikDetail", "viewAllRoute": "Gebruik", "viewAllQuery": { "consumer": "@objectId" }, "allowCreate": false, "emptyText": "No applications in use recorded for this organisation yet" } },
{ "id": "org-contactpersonen", "type": "object-list", "title": "Contact persons", "icon": "AccountGroup", "content": { "register": "@resolve:voorzieningen_register", "schema": "contactPerson", "filter": { "organization": "@objectId" }, "columns": [ { "key": "role", "label": "Function" }, { "key": "roles", "label": "Roles" } ], "limit": 25, "rowRoute": "ContactpersoonDetail", "emptyText": "No contact persons linked to this organisation yet" } }
{ "id": "org-modules", "type": "object-list", "title": "Applications", "icon": "Package", "content": { "register": "@resolve:voorzieningen_register", "schema": "module", "filter": { "provider": "@objectId" }, "columns": [ { "key": "name", "label": "Name" }, { "key": "licentietype", "label": "License type" }, { "key": "bbnLevel", "label": "BBN level" } ], "limit": 25, "rowRoute": "ModuleDetail", "viewAllRoute": "Modules", "viewAllQuery": { "provider": "@objectId" }, "emptyText": "No applications registered for this organisation yet" } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 GEMMA facet counts still ignore the provider narrowing from "View all" (spot-check of the open thread on #1275)

The organisation page's org-modules widget opens Modules with viewAllQuery: { provider: @objectId }. The list applies that key, but fetchFacets in src/store/modules/facets.js still sends only the _gf_* filters, the search term and organization, so the sidebar counts cover all applications while the table shows the organisation's. Unchanged at the head; cosmetic, as the thread already says.

Fix: forward non-_gf_ query keys to the facets request, or hide the sidebar while the list is pre-filtered. Fine to leave for a later change.

Done when: grep -n provider src/views/FacetedCatalogIndexView.vue src/store/modules/facets.js finds the key being forwarded, or the thread is closed as a known limitation.

Verification: grep -n provider src/views/FacetedCatalogIndexView.vue src/store/modules/facets.js -> no match at the head; fetchFacetsFromApi(schema, { filters, search, organization }) at facets.js:201

type="radio"
:disabled="importing"
data-testid="cmdb-import-missing-records-archive">
{{ t('stackiq', 'Archive or delete them') }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Without an archive sheet the option texts still promise the trash (spot-check of the open thread on #1288)

cmdbImport.js switches its info paragraph on archiveSheet (line ~1237 says "is archived" only when no archive sheet is configured), but the radio label "Archive or delete them" (line 141), its help "...archived or moved to the trash" (line 156) and the info line "...archived or deleted" (line ~420) in CmdbImport.vue do not. Still true at the head. The shipped profile always names archiveSheet, so it does not show today; it matters only once a profile without one is supported (the thread says so too).

Fix: pass the profile's archiveSheet into these three texts, or record in the profile docs that archiveSheet is required.

Done when: a mounted section with archiveSheet: null renders no "delete"/"trash" wording (add the case to CmdbImport.spec.js, which today asserts not.toContain('moved to the trash') only for the null-archive info panel at line 383).

Verification: git grep -n "Archive or delete them\|moved to the trash\|archived or deleted" -- src/views src/utils -> unconditional in CmdbImport.vue:141,156,420

{ "key": "module", "label": "Application", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "module", "labelField": "name" } },
{ "key": "moduleVersion", "label": "Version", "widget": "fkResolve", "widgetProps": { "register": "@resolve:voorzieningen_register", "schema": "moduleVersion", "labelField": "version" } },
"status",
{ "key": "businessOwner", "label": "Business owner", "widget": "contactName" },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 The Technical owner column of Applications in use was not given the contactName widget

#1292 fixes the Business owner column, which showed the contact person's uuid because a contactPerson carries only contactsUid (its objectNameField) and the name lives in Nextcloud Contacts. technicalOwner is the same $ref contactPerson relation, set through the same picker, but its column is still the plain key on the next line, so it presumably shows the same uuid. The PR description names only the business owner. Heads-up, not verified on a rig.

Fix: { "key": "technicalOwner", "label": "Technical owner", "widget": "contactName" }, plus the same in the organisation list if it shows owners.

Done when: on a rig, a usage with a technical owner that is in the viewer's address book shows the name in the list, and the uuid for a viewer without access.

Verification: sed -n 26,27p src/manifest.d/usages.json -> businessOwner has "widget": "contactName", technicalOwner is the bare string

Suggested change
{ "key": "businessOwner", "label": "Business owner", "widget": "contactName" },
{ "key": "businessOwner", "label": "Business owner", "widget": "contactName" },

},
"screen": {
"canvas": "design-system",
"board": "SkModuleversie"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 screen.board is a string on 118 capabilities and a list on 44; 41 decided-no rows carry screen: {}

The new screen block uses "board": "SkModuleversie" (a string) on 118 rows and "board": ["SkApplicaties", "SkCmdb"] (a list) on 44, while specScreens.*.board is always a list. A reader that iterates board reads a string character by character (I did, by accident, while counting boards). Also 41 decided-no rows have an empty screen object with no reason, whereas 13 other rows and 22 specScreens entries give a reason when there is no board. And four specs that sit under a feature (ai-system-inventory, contract-administration, maintenance-and-supplier-roadmap, open-data-publishing) have no specScreens entry although the commit says every spec is placed on a screen or given a reason; their rows may cover them, not verified.

Fix: make board always a list, give the empty screen rows a reason, and add the four specs to specScreens (or state the rule that a spec with rows needs no entry).

Done when: python3 -c over the file finds type(board) is list for every non-empty screen, and every spec dir under openspec/specs is a key of specScreens or named in a feature with a row on a board.

Verification: python3 -c "import json,collections;d=json.load(open('openspec/parity/capabilities.json'));print(collections.Counter(type((c.get('screen') or {}).get('board')).__name__ for c in d['capabilities']))" -> str 118, list 44, NoneType 54

|---|---|---|
| `maxFileBytes` | `10485760` (10 MB) | the size of the uploaded file |
| `maxRowsPerSheet` | `10000` | the rows with data on one CMDB sheet |
| `maxRowsPerSheet` | `10000` | the rows with data on one CMDB sheet, and on the archive sheet |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 "Five limits are read from the profile" above a table of six

The sentence at line 400 says five limits, the table under it has six rows (maxFileBytes, maxRowsPerSheet, maxUncompressedBytes, maxPartBytes, maxSharedStrings, maxReferencedStringBytes; the profile JSON has the same six). The sentence is older than these PRs (it was already five at the merge-base against six rows), the table rows are rewritten in #1285 (line 406 etc.), so it is worth fixing together. Anchored on the nearest changed line.

Fix: "Six limits".

Done when: git grep -n 'Five limits' -- docs finds nothing and the count matches the table.

Verification: git grep -n 'Five limits' -- docs openspec src lib -> docs/features/cmdb-import.md:400 only; grep -c '^| max' docs/features/cmdb-import.md` -> 6

- GIVEN the e2e THEN after the first import, importing the variant archives 1234 and soft-deletes 2, the Applications page lists 1234 under "Archived", and importing the original again reports 1234 `unarchived` and 2 `restored` with the object counts back to the first import's
- GIVEN the docs THEN "Repeat imports" describes archive, soft delete, the archive sheet, the option and the return of an application, and the error table has `ARCHIVE_UNAVAILABLE` and the new meaning of `MISSING_RECORDS_UNSUPPORTED`
- [x] Implement
- [ ] Test

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 The archive-reconciliation Playwright test was written but never run, and the two import changes have not been exercised together

Task 7's - [ ] Test says the Playwright test (tests/e2e/spec-coverage/cmdb-import.spec.ts: archive 1234, soft-delete 2, bring both back) is "written and not run yet"; only the PHPUnit fixture round trip ran. The mapping-view change (#1288/#1289) then moved the packs the import runs into OpenRegister's store, so the combined path (stored packs + missingRecords=archive default + OpenRegister's ArchiveHandler, which throws when a schema lacks x-openregister-archive) exists only as unit tests with doubles. The checkbox is honest, so this is a gap in evidence, not in the tasks file. The release also changes the default for every beta import to archive.

Fix: run cmdb-import.spec.ts and cmdb-import-mapping.spec.ts on a rig with the head of development (hardened OpenRegister, module schema 0.3.9, usage 1.5.7) before the release; tick Task 7's Test.

Done when: both specs pass on that rig and the checkbox is ticked.

Verification: grep -n '^- \[ \]' openspec/changes/cmdb-import-archive-reconciliation/tasks.md -> line 87 only, with the 'not run yet' status below it

WilcoLouwerse
WilcoLouwerse previously approved these changes Oct 15, 2026

@WilcoLouwerse WilcoLouwerse left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE (Thorough)

Ready for beta: the CMDB import work (WOO-587 / WOO-588) composes cleanly on development, and what remains is seven minors, two of them earlier threads that still hold.

🟢 Minor (7)

  • Facet counts ignore the provider narrowing from View all — src/manifest.json:424 (introduced in #1275). The organisation page's org-modules widget opens Modules with viewAllQuery: { provider: @objectId }.
  • Option texts still promise the trash without an archive sheet — src/views/settings/sections/CmdbImport.vue:141 (introduced in #1287). cmdbImport.js switches its info paragraph on archiveSheet (line ~1237 says "is archived" only when no archive sheet is configured), but the radio label "Archive or delete them" (line 141), its help "...archived or moved to the trash" (line 156) and the inf
  • Technical owner column still shows the raw uuid — src/manifest.d/usages.json:26 (introduced in #1292). #1292 fixes the Business owner column, which showed the contact person's uuid because a contactPerson carries only contactsUid (its objectNameField) and the name lives in Nextcloud Contacts.
  • Parity matrix: screen.board is string or list — openspec/parity/capabilities.json:939 (introduced in #1276). The new screen block uses "board": "SkModuleversie" (a string) on 118 rows and "board": ["SkApplicaties", "SkCmdb"] (a list) on 44, while specScreens.*.board is always a list.
  • Capability area differs from its feature's area (34 rows) — openspec/parity/capabilities.json:7003 (introduced in #1273, #1276), no changed line within reach, so no thread. Every capability now names one feature, and every feature has an area, but for 34 of 216 rows capability.area != feature.area, for example land-usage-record (landscape) under offering-and-usage-listings (market), ins-faceted-search and ins-saved-view (insight) under an architecture feature, sec-multi-factor-sign-in (security) under an organisations feature. If the matrix is meant to roll up by feature area, those rows are counted in the wrong area; if a feature may span areas, the feature's single area field is misleading. Row ids, ratings and built states are otherwise unchanged by these PRs (diffed per id against the merge-base: only screen, feature, featureConfidence changed). Fix: either move the 34 rows' area (or feature) so they agree, or document that feature.area is the feature's home and capability.area may differ. Done when: a script comparing capability.area with its feature's area prints no rows, or the rule is written next to features. Verification: python3 -c joining capabilities to features on feature -> 34 mismatches (list in this finding's verification)
  • Docs say five limits, table lists six — docs/features/cmdb-import.md:406 (introduced in #1285). The sentence at line 400 says five limits, the table under it has six rows (maxFileBytes, maxRowsPerSheet, maxUncompressedBytes, maxPartBytes, maxSharedStrings, maxReferencedStringBytes; the profile JSON has the same six).
  • Archive reconciliation e2e never run, never with stored packs — openspec/changes/cmdb-import-archive-reconciliation/tasks.md:87 (introduced in #1286, #1287). Task 7's - [ ] Test says the Playwright test (tests/e2e/spec-coverage/cmdb-import.spec.ts: archive 1234, soft-delete 2, bring both back) is "written and not run yet"; only the PHPUnit fixture round trip ran.

✅ Verified clean

  • The stacked #1286, #1287 and #1289 reached development: each PR's distinctive symbols occur exactly once at the head, and the stack merges left no duplicate definitions or JSON keys.
  • WOO-587 / WOO-588 seam: an admin-edited OpenRegister pack cannot move the reconcile key (externalNumber comes from the profile key column; externalKey, module and consumer are refused).
  • The TOPdesk profile version going from 2.0.0 (beta) to 1.0.0 is cosmetic: the pack store decides seed, update and edited by a sha256 of the definition, and nothing compares versions.
  • Dependabot #1035 (guzzle ^7.8 → ^8.2) is require-dev only; the one integration test uses verbs and an auth array that Guzzle 8.2 still supports.
  • package-lock.json pins @conduction/nextcloud-vue 2.73.1.

Scope decision: promotion of 154 commits, 0 direct. 15 merged PRs: the 10 CMDB-import PRs by WilcoLouwerse (#1271–#1292, ~33k lines) trusted on their earlier self-review after a spot-check; Ruben's #1268, #1273, #1276, #1283 and dependabot's #1035 read at full depth, plus an integration pass. Residual: the reconcile and transition bodies in CmdbExportImportService, CmdbWorkbookReader, the profile validation internals and the tests of the self-reviewed PRs were not re-read.
Gates: quality / Hydra Gates and quality / Quality Report green on 7bc44c09.
Before the next round: the archive-reconciliation Playwright test has never run; worth one run on a rig before beta → main.

…1039)

Bumps [conduction/hydra-gates](https://github.com/ConductionNL/.github) from 1.16.1 to 1.18.0.
- [Release notes](https://github.com/ConductionNL/.github/releases)
- [Commits](ConductionNL/.github@v1.16.1...v1.18.0)

---
updated-dependencies:
- dependency-name: conduction/hydra-gates
  dependency-version: 1.18.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
}

$target = CmdbImportReport::ARCHIVED;
if ($archive['present'] === true && $archive['uncachedRows'] === [] && $this->unreadableKeyRows === []) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 A workbook without one of the two CMDB sheets soft-deletes every application from that sheet, with no warning

CmdbWorkbookReader::sourceSheets() keeps array_intersect($expected, $available) and throws NO_SOURCE_SHEET only when none of the profile's sheets is present. So a workbook that has Onbeh Applicaties CMDB and Gearchiveerde Applicaties but not Beheerde Applicaties CMDB (the sheet was renamed, or someone exported a partial workbook) is read without an error or a warning. The only warning for a missing sheet in the reader is the one for an optional column (line 862).

In reconcileTarget() the workbook counts as authoritative for deletion when the archive sheet is present and no key was unreadable. Whether every source sheet was there is never checked. As a result, every application that an earlier import created from the absent sheet is missing from winningSheets and moves to DELETED: its usage and module are soft-deleted. missingRecords defaults to archive (DEFAULT_MISSING_RECORDS), so this is what happens on a plain re-import. The objects are recoverable from the trash, but the report does not say why they left. No unit test covers a workbook with only some of the source sheets.

Fix: have the reader return the expected sheets that are absent. Treat a missing sheet like an unreadable key: never DELETED (archive at most, or skip reconciliation), and add an import warning naming the sheet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked as #1304: filed rather than fixed here because the fix lands through development and this release PR is re-cut after every merge.

@rjzondervan rjzondervan left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST_CHANGES (Quick)

Not ready for beta: a CMDB workbook that lacks one of its source sheets imports without an error and soft-deletes every application that sheet created, on a plain re-import with the default missingRecords: archive.

🔴 Blockers (1)

  • A missing CMDB source sheet soft-deletes all its applications, silently — lib/Service/CmdbExportImportService.php:2681 (introduced in #1288). The reader refuses a workbook only when none of its source sheets is present, and the reconciliation scope is every earlier import, so a workbook missing one sheet soft-deletes every application that sheet created, with no reason in the report. Tracked in #1304.

Tracking issues

  • #1304 — A missing CMDB source sheet soft-deletes all its applications, silently

Scope decision: full, provenance-driven review at Quick strictness (definite blockers only), in two slices (backend; frontend, manifest and docs). 16 merged PRs, 0 direct commits. Most of the code (#1285–#1292) was written by Wilco, so his dismissed approval of this release was a self-review. His 6 open threads were not repeated.
Checked and holding: guzzle 8 is dev-only and unused in lib/; the dashboard-widget repair step is idempotent; the mapping endpoint is admin-only; reconciliation is limited to this municipality's own imports; every new manifest key and frontend endpoint resolves.
Not read: about 40 files (the CmdbImportPack* classes, contact sync, docs, most tests).
CI: all checks green on af446eb; E2E (Playwright) was skipped, and the earlier status comment made approval conditional on a green E2E run.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants