Skip to content

feat(settings): every connection on one page, honestly - #1939

Merged
rubenvdlinde merged 13 commits into
developmentfrom
feat/integrations-page
Sep 8, 2026
Merged

feat(settings): every connection on one page, honestly#1939
rubenvdlinde merged 13 commits into
developmentfrom
feat/integrations-page

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Implements the merged OpenSpec change pluggable-integration-registry (round 2, row A34), which extends admin-settings.

Dossiq talks, or claims to talk, to ten systems outside it. The only place any of them appeared was /settings/admin/dossiq: seventeen sections in 12,371 characters, no navigation, and no answer to the question an admin actually has. This adds one page under the gear that lists every connection with a status the app can back, and a link to the section that configures it.

What the page says, and why that is the point

Per M3-integrations.md the dossiq column reads partial, stub for eight of the ten and spec only for BRP and KvK. So the seed says exactly that:

  • BRP and KvK are Not available with "Specified, not built yet", and offer no settings link.
  • The other eight are Not configured with "Not checked yet".
  • No row is seeded Configured. Only a save or a probe may write that.

The StUF endpoint health and the mailbox Test connection now write their outcome. A save of a section without a probe records Configured when its required keys are filled and Not configured when they are cleared, and only for the connections whose own keys the payload carried, so saving the KCC form never restates the ZGW card.

Three places the tree disagreed with the design, and what shipped instead

1. It is a type: "index" page, not type: "settings". In the installed @conduction/nextcloud-vue a settings section's widgets[] resolves only against BUILTIN_SETTINGS_WIDGETS (version-info, register-mapping) and the component discriminator; object-list is registered with registerDashboardWidget(). CnSettingsPage takes no page-level widgets[] prop either. The widget would have rendered an empty section behind one console warn. Task 2.2 named this outcome in advance and licensed the index page; a custom page was never an option (ADR-100 ratchet).

The cost is REQ-ADMIN-021, the Required apps section, which stays RED. It needs CnLeafDependencySettings through a type: "component" widget, and an index page has nowhere to mount it. Its e2e scenario is test.fixme naming this reason. Nothing else about the page has to change when a settings section can host a list.

2. Open settings is a column, not a row action. A row action cannot carry a per-row URL — type: "navigate" takes one literal target and pushes it through vue-router, which cannot reach /settings/admin/dossiq at all — nor a per-row visibility, since CnRowActions.visible is a function and the action schema is additionalProperties: false. The built-in link cell widget substitutes {settingsUrl} from the row and falls through to plain text when it is empty, which is exactly what REQ-ADMIN-019 asks of the BRP and KvK rows: the affordance absent, not disabled.

3. Seven anchors, not the design's eight, and PDOK has no settings link. There is no PDOK section on the admin page: its pdok_* keys are read by the map components and written by nothing an admin can open. Linking to #section-pdok would have been the exact class of quiet untruth this page exists to remove. The design also reports two duplicated headings; every CnSettingsSection name in AdminRoot.vue is in fact unique, so nothing was deduplicated, and a unit test now pins the ids unique.

Where the recording is called from

Not the constructors the design assumed. EmailTemplateController, SettingsController and StoreController each sit at PHPMD's CouplingBetweenObjects ceiling of 13; injecting the recorder turned all three red, measured, and all three were clean before. Each now records through a collaborator it already holds — EmailTemplateService::recordMailboxStatus() and ConfiguredRegistryService::recordStoreStatus(), which is the better home anyway since "is a registry configured" is that service's own question. SettingsController has neither and resolves the recorder from the container by name; even a catch (\Throwable) there is a type reference that pushed the count back over, so the guard is has(), and a unit test asserts the string FQCN resolves to the real class so it cannot rot into a silent no-op. SettingsService was the obvious seam and could not take it: it is at all three PHPMD ceilings at once (999 lines, complexity 49, coupling 12).

Verification, by exit code

eslint 0 · prettier 0 · vitest 0 (743) · check:manifest 0 · check-l10n 0 · check:schema-l10n 0 (at baseline) · check:l10n-js 0 · phpcs 0 · phpmd 0 per directory plus the unused-params leg · psalm 0 · phpstan 0 · phpunit 0 (3236) · hydra gates with --base origin/development.

composer check:strict was not the command run: it exceeds the 300s tool timeout, and phpmd printing nothing inside it is its OOM signature rather than a pass, so the legs were run individually. Playwright was not runtests/e2e/integrations-page.spec.ts is new and unexecuted here, and E2E runs on the development push, not on this PR.

gate-101 (demo-data-coverage) failed the first real run: the new schema had zero demo objects. Fixed by hand rather than by the generator, which rewrote 22,025 lines even under --keep because development's mock register is ten schemas behind the real ones. The three rows are validated by the generator's own --check.

🤖 Generated with Claude Code

Dossiq talks, or claims to talk, to ten systems outside it, and until now
the only place any of them appeared was a 12,371-character admin page with
no navigation and no answer to the one question an admin has: does this
work. This is the noun that question needs.

The `dossiqIntegration` schema carries a connection's key, title, status,
status message, checked-at and the settings section that configures it.
The status enum has four values and that is the whole vocabulary:
Configured, Not configured, Not available, Error.

The seed is where the honesty lives. Per M3-integrations.md the dossiq
column reads `partial, stub` for eight of the ten and `spec only` for BRP
and KvK, so BRP and KvK are seeded Not available with "Specified, not built
yet" and carry no settings link, and NO row is seeded Configured. Only a
save or a probe may write that. A page that starts by claiming eight
working connections would be worse than no page.

PDOK is seeded with an empty settings link too, and that one is a finding
rather than a decision: the design counted eight admin sections to anchor
and there are seven. PDOK's `pdok_*` keys are read by the map components
and written by nothing an admin can open. Linking to a section that does
not exist is precisely the class of quiet untruth this page exists to
remove, so the link is absent and the seed says why.

The register version moves to 1.2.0 and the config to 0.15.0 so the import
does not fast-skip the new schema.
The page is an index over the config schema, admin only, in the settings
foldout. It shows the connection, its status, what the last probe or save
said, when that was, and a link to the section that configures it.

IT IS NOT THE `type: "settings"` PAGE THE DESIGN ASKED FOR, and the reason
is worth keeping rather than rediscovering. In the installed
@conduction/nextcloud-vue a settings section's widgets resolve only against
`version-info`, `register-mapping` and the `component` discriminator, while
`object-list` is registered with `registerDashboardWidget()`; CnSettingsPage
takes no page-level widgets prop either. The widget would have rendered an
empty section behind a single console warn. Task 2.2 named this outcome in
advance and licensed the index page; a custom page was never an option.
The cost is REQ-ADMIN-021, the Required apps section, which stays red.

Open settings is a COLUMN, not a row action, and that is the second thing
the tree teaches. A row action cannot carry a per-row URL: `type:
"navigate"` takes one literal target and pushes it through vue-router,
which cannot reach /settings/admin/dossiq at all. Nor can it carry a
per-row visibility: CnRowActions' `visible` predicate is a function, and
JSON cannot hold one. The built-in `link` widget substitutes {settingsUrl}
from the row and falls through to plain text when that is empty, which is
exactly what the spec asks of the BRP and KvK rows: the affordance absent,
not disabled.

AdminRoot.vue gains the seven anchors those links point at. The design's
eighth, PDOK, has no section to anchor.

The vitest spec guards the failures that are silent in a browser: an icon
outside src/icons.js renders nothing, a menu id outside settingsSection
lands in the main nav where every user sees it, and a settings link to a
missing anchor scrolls nowhere.
A page that only ever shows its seed is worse than no page, because it
looks like an answer. IntegrationStatusService is the one write path: it
refuses a key the seed does not ship and a status outside the four, sends
the WHOLE row back because saveObject is PUT-semantic and a partial write
would blank the title the page renders, and swallows its own failures. That
last one matters: it runs beside a connection test whose answer is what the
admin actually asked for, and a page that cannot be updated must not turn a
working probe into a 500.

The StUF endpoint list and the mailbox test now record what they found. A
save of a section without a probe records Configured when its required
keys are filled and Not configured when they are cleared, and ONLY for the
connections whose own keys the payload carried, so saving the KCC form
never restates the ZGW card.

WHERE THAT WRITE IS CALLED FROM IS NOT WHERE THE DESIGN PUT IT.
EmailTemplateController, SettingsController and StoreController each sit at
PHPMD's CouplingBetweenObjects ceiling of 13. Injecting the recorder turned
all three red, measured, and all three were clean before. So each records
through a collaborator it already holds: EmailTemplateService for the
mailbox, ConfiguredRegistryService for the Store — which is the better home
anyway, since "is a registry configured" is that service's own question.
SettingsController has neither and resolves the recorder from the container
BY NAME; even a `catch (\Throwable)` there is a type reference that pushes
the count back over, so the guard is has(). A string FQCN is exactly the
kind of reference that rots into a silent no-op after a rename, so a test
asserts it resolves to the real class.

SettingsService was the obvious seam and could not take it: it is at all
three of PHPMD's ceilings at once — 999 lines, complexity 49, coupling 12.
Every scenario the delta spec names, asserted on ids, hrefs and the objects
the API holds rather than on English labels — nothing forces the language
of the E2E instance, and five specs went red on a Dutch one before.

The non-admin scenario needs a user who is not an admin, so ci-seed.sh
creates one. Asserting the guard from the admin session would have been no
test at all: the admin sees everything, so it would pass on an app with no
permission checks whatsoever. The request context sends its credentials
with send:'always' because dossiq answers 401 without a WWW-Authenticate
header, and Playwright would otherwise never send them.

The Required apps scenario is test.fixme naming its reason: REQ-ADMIN-021
needs CnLeafDependencySettings on a settings page, the library cannot host
the connections there, and an index page has nowhere to put the section.

The KCC scenario writes app config that outlives the run, so a last test
puts the row back where the "claims nothing it has not checked" assertion
needs it on a reused instance.
ADR-111 rule 1 wants three demo objects per schema, and gate-101 found the
new one with zero. It failed the run; the earlier run that exited 0 was
baseless and had judged nothing.

The generator was NOT used to fix it. Both `--objects` and `--keep` rewrote
the whole descriptor — 22,025 deletions on a --keep run — because
development's mock register is ten schemas behind the real ones and the
regeneration would have swept in everyone else's drift under this change's
name. The three rows are inserted instead, at the file's own indent so the
diff is 132 lines and nothing else moves, and `--check` validates them
against the schema that will hold them.

They are demo data and they read like it, but the STATES are chosen: one
Not configured, one Error, one Not available, so a demo shows the three
things a card can honestly say without any of them claiming a working
connection.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ c157d51

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 13:37 UTC

Download the full PDF report from the workflow artifacts.

…ons-page

# Conflicts:
#	l10n/en.js
#	l10n/en.json
#	l10n/nl.js
#	l10n/nl.json
…t moved

Two things the merge of development brought, both of which fail loudly and
neither of which is about this change's behaviour.

`manifestCaseHeader.spec.js` (case-identity, merged an hour ago) pins the
manifest at 44 pages so that a subtitle change cannot quietly move anything
else. Integrations makes it 45. The number is raised by exactly the page
that was added rather than loosened to a range, because the pin is the
assertion's whole value.

And `l10n/en.js` / `nl.js` were stale against their JSON: the built browser
catalogue is generated, and a generated blob committed on a branch goes
stale the moment the base adds a key. CI caught it before I did, on the
merge commit rather than on the commit I tested — which is the point of a
`pull_request` check testing your branch merged INTO the base.
Six PHPUnit cells were red on this PR and the summary line said OK. The exit
code was 1: `failOnRisky` plus `beStrictAboutCoverageMetadata`, and
`IntegrationProbesRecordTest` executed `StufServices` without declaring it.

WHY IT HAS TO BE REAL. `StufServices` is an immutable bundle of eight
collaborators with public readonly promoted properties.
`StufControllerContractTest` can mock it because its tests never read one;
mine calls `endpoints()`, which reads `->register` and `->circuitBreaker`,
and a mock leaves a readonly promoted property uninitialised and throws
before the assertion. So it is built, and now named in `@uses` — the same
dialect #1943 used on the case-type tests an hour ago.

CI NAMED TWO OF THE THREE TESTS THAT BUILD IT, so the fix is the class-level
declaration rather than the two tests it happened to name: an attribution
that differs between runs is not a list to work through.

WHAT I COULD AND COULD NOT VERIFY LOCALLY, because it is the reason this
reached CI at all. This box has no pcov or xdebug and no sudo to install
one, and `--no-coverage` disables the coverage-metadata check outright — so
every local run I made was structurally incapable of seeing this, and
printed OK with exit 0. What IS verified locally is the half that has bitten
before: PHPUnit's own metadata parser resolves the annotation to
'\\OCA\\Dossiq\\Service\\Stuf\\StufServices' rather than to an empty
`@uses`, and that class exists. The rest is CI's to answer.
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ 9370c55

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 14:26 UTC

Download the full PDF report from the workflow artifacts.

…ons-page

# Conflicts:
#	l10n/en.js
#	l10n/en.json
#	l10n/nl.js
#	l10n/nl.json
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/dossiq @ acc3a75

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue3-compile
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 541/541
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman ⏭️
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-08 14:35 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 65a6275 into development Sep 8, 2026
49 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/integrations-page branch September 8, 2026 14:40
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.

1 participant